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

Java tutorial

Introduction

Here is the source code for com.facebook.swift.codec.metadata.AbstractThriftMetadataBuilder.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.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
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 com.google.inject.internal.MoreTypes;

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

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.ParameterizedType;
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 static com.facebook.swift.codec.ThriftField.Requiredness;
import static com.facebook.swift.codec.metadata.FieldMetadata.extractThriftFieldName;
import static com.facebook.swift.codec.metadata.FieldMetadata.getOrExtractThriftFieldName;
import static com.facebook.swift.codec.metadata.FieldMetadata.getThriftFieldId;
import static com.facebook.swift.codec.metadata.FieldMetadata.getThriftFieldIsLegacyId;
import static com.facebook.swift.codec.metadata.FieldMetadata.getThriftFieldName;
import static com.facebook.swift.codec.metadata.FieldKind.THRIFT_FIELD;
import static com.facebook.swift.codec.metadata.FieldMetadata.*;
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.ReflectionHelper.resolveFieldTypes;
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 abstract class AbstractThriftMetadataBuilder {
    protected final String structName;
    protected final Type structType;
    protected final Type builderType;

    protected final List<String> documentation;
    protected final List<FieldMetadata> fields = newArrayList();

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

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

    protected final ThriftCatalog catalog;
    protected final MetadataErrors metadataErrors;

    protected AbstractThriftMetadataBuilder(ThriftCatalog catalog, Type structType) {
        this.catalog = checkNotNull(catalog, "catalog is null");
        this.structType = checkNotNull(structType, "structType is null");
        this.metadataErrors = new MetadataErrors(catalog.getMonitor());

        // assign the struct name from the annotation or from the Java class
        structName = extractName();
        // get the builder type from the annotation or from the Java class
        builderType = extractBuilderType();
        // grab any documentation from the annotation or saved JavaDocs
        documentation = ThriftCatalog.getThriftDocumentation(getStructClass());
        // 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();
    }

    protected abstract String extractName();

    protected abstract Class<?> extractBuilderClass();

    protected abstract void validateConstructors();

    protected abstract boolean isValidateSetter(Method method);

    protected abstract ThriftFieldMetadata buildField(Collection<FieldMetadata> input);

    public abstract ThriftStructMetadata build();

    public MetadataErrors getMetadataErrors() {
        return metadataErrors;
    }

    public Class<?> getStructClass() {
        return TypeToken.of(structType).getRawType();
    }

    public Class<?> getBuilderClass() {
        return TypeToken.of(builderType).getRawType();
    }

    private Type extractBuilderType() {
        Class<?> builderClass = extractBuilderClass();

        if (builderClass == null) {
            return null;
        }

        if (builderClass.getTypeParameters().length == 0) {
            return builderClass;
        }

        if (!(structType instanceof ParameterizedType)) {
            metadataErrors.addError(
                    "Builder class '%s' may only be generic if the type it builds ('%s') is also generic",
                    builderClass.getName(), getStructClass().getName());
            return builderClass;
        }

        if (builderClass.getTypeParameters().length != getStructClass().getTypeParameters().length) {
            metadataErrors.addError(
                    "Generic builder class '%s' must have the same number of type parameters as the type it builds ('%s')",
                    builderClass.getName(), getStructClass().getName());
            return builderClass;
        }

        ParameterizedType parameterizedStructType = (ParameterizedType) structType;

        return new MoreTypes.ParameterizedTypeImpl(builderClass.getEnclosingClass(), builderClass,
                parameterizedStructType.getActualTypeArguments());
    }

    protected final void verifyClass(Class<? extends Annotation> annotation) {
        String annotationName = annotation.getSimpleName();
        String structClassName = getStructClass().getName();

        // Verify struct class is public and final
        if (!Modifier.isPublic(getStructClass().getModifiers())) {
            metadataErrors.addError("%s class '%s' is not public", annotationName, structClassName);
        }

        if (!getStructClass().isAnnotationPresent(annotation)) {
            metadataErrors.addError("%s class '%s' does not have a @%s annotation", annotationName, structClassName,
                    annotationName);
        }
    }

    protected final void extractFromConstructors() {
        if (builderType == null) {
            // struct class must have a valid constructor
            addConstructors(structType);
        } else {
            // builder class must have a valid constructor
            addConstructors(builderType);

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

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

    protected final void addConstructors(Type type) {
        Class<?> clazz = TypeToken.of(type).getRawType();

        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(type,
                    constructor.getParameterAnnotations(),
                    resolveFieldTypes(structType, 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());
            }
        }

        validateConstructors();
    }

    protected final void addBuilderMethods() {
        for (Method method : findAnnotatedMethods(getBuilderClass(), ThriftConstructor.class)) {
            List<ParameterInjection> parameters = getParameterInjections(builderType,
                    method.getParameterAnnotations(),
                    resolveFieldTypes(builderType, method.getGenericParameterTypes()),
                    extractParameterNames(method));

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

            if (!getStructClass().isAssignableFrom(method.getReturnType())) {
                metadataErrors.addError(
                        "'%s' says that '%s' is its builder class, but @ThriftConstructor method '%s' in the builder does not build an instance assignable to that type",
                        structType, builderType, method.getName());
            }
        }

        // find invalid methods not skipped by findAnnotatedMethods()
        for (Method method : getAllDeclaredMethods(getBuilderClass())) {
            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",
                    getBuilderClass().getName());
        }
        if (builderMethodInjections.size() > 1) {
            metadataErrors.addError("Multiple builder methods are annotated with @ThriftConstructor ",
                    builderMethodInjections);
        }
    }

    protected final void extractFromFields() {
        if (builderType == null) {
            // struct fields are readable and writable
            addFields(getStructClass(), true, true);
        } else {
            // builder fields are writable
            addFields(getBuilderClass(), false, true);
            // struct fields are readable
            addFields(getStructClass(), true, false);
        }
    }

    protected final 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());
                }
            }
        }
    }

    protected final 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(structType, fieldField, annotation, THRIFT_FIELD);
            fields.add(fieldExtractor);
            extractors.add(fieldExtractor);
        }
        if (allowWriters) {
            FieldInjection fieldInjection = new FieldInjection(structType, fieldField, annotation, THRIFT_FIELD);
            fields.add(fieldInjection);
            fieldInjections.add(fieldInjection);
        }
    }

    protected final void extractFromMethods() {
        if (builderType != null) {
            // builder methods are writable
            addMethods(builderType, false, true);
            // struct methods are readable
            addMethods(structType, true, false);
        } else {
            // struct methods are readable and writable
            addMethods(structType, true, true);
        }
    }

    protected final void addMethods(Type type, boolean allowReaders, boolean allowWriters) {
        Class<?> clazz = TypeToken.of(type).getRawType();

        for (Method fieldMethod : findAnnotatedMethods(clazz, ThriftField.class)) {
            addMethod(type, 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());
                }
            }
        }
    }

    protected final void addMethod(Type type, Method method, boolean allowReaders, boolean allowWriters) {
        checkArgument(method.isAnnotationPresent(ThriftField.class));

        ThriftField annotation = method.getAnnotation(ThriftField.class);
        Class<?> clazz = TypeToken.of(type).getRawType();

        // verify parameters
        if (isValidateGetter(method)) {
            if (allowReaders) {
                MethodExtractor methodExtractor = new MethodExtractor(type, method, annotation, THRIFT_FIELD);
                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(type, method.getParameterAnnotations(),
                            resolveFieldTypes(type, 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.requiredness() == Requiredness.REQUIRED) {
                        metadataErrors.addError(
                                "A method with annotated parameters can not be marked as required: %s.%s ",
                                clazz.getName(), method.getName());
                    }
                } else {
                    Type parameterType = resolveFieldTypes(type, method.getGenericParameterTypes())[0];
                    parameters = ImmutableList.of(new ParameterInjection(type, 0, annotation,
                            ReflectionHelper.extractFieldName(method), parameterType));
                }
                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());
        }
    }

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

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

    protected final List<ParameterInjection> getParameterInjections(Type type, 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(type, parameterIndex, thriftField,
                    parameterNames[parameterIndex], parameterType);

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

    protected final 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("Thrift class '%s' fields %s do not have an id", structName,
                                newTreeSet(transform(fields, getOrExtractThriftFieldName())));
                    }
                }
                continue;
            }

            short fieldId = entry.getKey().get();

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

            // ensure all fields for this ID have the same requiredness
            Requiredness requiredness = extractFieldRequiredness(fieldId, fieldName, fields);
            for (FieldMetadata field : fields) {
                field.setRequiredness(requiredness);
            }

            // We need to do the isLegacyId check in two places. We've already done this
            // process for fields which had multiple `@ThriftField` annotations when we
            // assigned them all the same ID. It doesn't hurt to do it again. On the other
            // hand, we need to do it now to catch the fields which only had a single
            // @ThriftAnnotation, because inferThriftFieldIds skipped them.
            boolean isLegacyId = extractFieldIsLegacyId(fieldId, fieldName, fields);
            for (FieldMetadata field : fields) {
                field.setIsLegacyId(isLegacyId);
            }

            // 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.
     */
    protected final 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;
    }

    protected final 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();
            String fieldName = entry.getKey();

            // 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) {
                if (!fieldsWithConflictingIds.contains(fieldName)) {
                    metadataErrors.addError("Thrift class '%s' field '%s' has multiple ids: %s", structName,
                            fieldName, ids.toString());
                    fieldsWithConflictingIds.add(fieldName);
                }
                continue;
            }

            // single id, so set on all fields in this group (groups with no id are handled later),
            // and validate isLegacyId is consistent and correct.
            if (ids.size() == 1) {
                short id = Iterables.getOnlyElement(ids);

                boolean isLegacyId = extractFieldIsLegacyId(id, fieldName, fields);

                // propagate the id data to all fields in this group
                for (FieldMetadata field : fields) {
                    field.setId(id);
                    field.setIsLegacyId(isLegacyId);
                }
            }
        }
    }

    protected final boolean extractFieldIsLegacyId(short id, String fieldName, Collection<FieldMetadata> fields) {
        Set<Boolean> isLegacyIds = ImmutableSet
                .copyOf(Optional.presentInstances(transform(fields, getThriftFieldIsLegacyId())));

        if (isLegacyIds.size() > 1) {
            metadataErrors.addError("Thrift class '%s' field '%s' has both isLegacyId=true and isLegacyId=false",
                    structName, fieldName);
        }
        if (id < 0) {
            if (!isLegacyIds.contains(true)) {
                metadataErrors.addError(
                        "Thrift class '%s' field '%s' has a negative field id but not isLegacyId=true", structName,
                        fieldName);
            }
        } else {
            if (isLegacyIds.contains(true)) {
                metadataErrors.addError(
                        "Thrift class '%s' field '%s' has isLegacyId=true but not a negative field id", structName,
                        fieldName);
            }
        }

        return id < 0;
    }

    protected final 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("Thrift class %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;
    }

    protected final Requiredness extractFieldRequiredness(short fieldId, String fieldName,
            Collection<FieldMetadata> fields) {
        Predicate<Requiredness> specificRequiredness = new Predicate<Requiredness>() {
            @Override
            public boolean apply(@Nullable Requiredness input) {
                return (input != null) && (input != Requiredness.UNSPECIFIED);
            }
        };

        Set<Requiredness> requirednessValues = ImmutableSet
                .copyOf(filter(transform(fields, getThriftFieldRequiredness()), specificRequiredness));

        if (requirednessValues.size() > 1) {
            metadataErrors.addError("Thrift class '%s' field '%s(%d)' has multiple requiredness values: %s",
                    structName, fieldName, fieldId, requirednessValues.toString());
        }

        Requiredness resolvedRequiredness;
        if (requirednessValues.isEmpty()) {
            resolvedRequiredness = Requiredness.NONE;
        } else {
            resolvedRequiredness = requirednessValues.iterator().next();
        }

        return resolvedRequiredness;
    }

    /**
     * Verifies that the the fields all have a supported Java type and that all fields map to the
     * exact same ThriftType.
     */
    protected final 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("Thrift class '%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.addError("Thrift class '%s' field '%s(%s)' has multiple types: %s", structName, name,
                        id, types);
            }
        }
    }

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

    protected final 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);
                    }
                });
    }

    protected final 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()));
            }
        });
    }

    protected final 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());
            }
        });
    }
}