content: /*
   * 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 io.airlift.drift.codec.metadata;
  
  import com.google.common.collect.ImmutableList;
  import com.google.common.collect.ImmutableMap;
  import com.google.common.collect.Iterables;
  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 io.airlift.drift.annotations.ThriftConstructor;
  import io.airlift.drift.annotations.ThriftField;
  
  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.ArrayList;
  import java.util.Collection;
  import java.util.HashSet;
  import java.util.List;
  import java.util.Map;
  import java.util.Map.Entry;
  import java.util.Objects;
  import java.util.Optional;
  import java.util.Set;
  import java.util.TreeSet;
  
  import static com.google.common.base.Preconditions.checkArgument;
  import static io.airlift.drift.annotations.ThriftField.Requiredness;
  import static io.airlift.drift.codec.metadata.FieldKind.THRIFT_FIELD;
  import static io.airlift.drift.codec.metadata.ReflectionHelper.extractParameterNames;
  import static io.airlift.drift.codec.metadata.ReflectionHelper.findAnnotatedMethods;
  import static io.airlift.drift.codec.metadata.ReflectionHelper.getAllDeclaredFields;
  import static io.airlift.drift.codec.metadata.ReflectionHelper.getAllDeclaredMethods;
  import static io.airlift.drift.codec.metadata.ReflectionHelper.resolveFieldTypes;
  import static java.util.Objects.requireNonNull;
  import static java.util.stream.Collectors.groupingBy;
  import static java.util.stream.Collectors.toCollection;
  import static java.util.stream.Collectors.toList;
  import static java.util.stream.Collectors.toSet;
  
  // This class is not thread safe
  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 = new ArrayList<>();
  
      // readers
      protected final List<Extractor> extractors = new ArrayList<>();
  
      // writers
      protected final List<MethodInjection> builderMethodInjections = new ArrayList<>();
      protected final List<ConstructorInjection> constructorInjections = new ArrayList<>();
      protected final List<FieldInjection> fieldInjections = new ArrayList<>();
      protected final List<MethodInjection> methodInjections = new ArrayList<>();
  
      protected final ThriftCatalog catalog;
      protected final MetadataErrors metadataErrors;
  
      protected AbstractThriftMetadataBuilder(ThriftCatalog catalog, Type structType)
      {
          this.catalog = requireNonNull(catalog, "catalog is null");
          this.structType = requireNonNull(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 Map<String, String> extractStructIdlAnnotations();
  
      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.getParameterCount() > 1) || hasThriftFieldAnnotation(method)) {
                      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, List<String> parameterNames)
      {
          List<ParameterInjection> parameters = new ArrayList<>(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.get(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
          Map<Optional<Short>, List<FieldMetadata>> fieldsById = fields.stream()
                  .collect(groupingBy(field -> Optional.ofNullable(field.getId())));
          for (Entry<Optional<Short>, List<FieldMetadata>> entry : fieldsById.entrySet()) {
              Collection<FieldMetadata> fields = entry.getValue();
  
              // fields must have an id
              if (!entry.getKey().isPresent()) {
                  Set<String> sortedFields = fields.stream()
                          .map(FieldMetadata::getOrExtractThriftFieldName)
                          .collect(toCollection(TreeSet::new));
                  for (String fieldName : sortedFields) {
                      // 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, sortedFields);
                      }
                  }
                  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);
              }
  
              Map<String, String> idlAnnotations = extractFieldIdlAnnotations(fieldId, fields);
              for (FieldMetadata field : fields) {
                  field.setIdlAnnotations(idlAnnotations);
              }
  
              // ensure all fields for this ID have the same non-null get for isRecursiveReference
              boolean isRecursiveReference = extractFieldIsRecursiveReference(fieldId, fields);
              for (FieldMetadata field : fields) {
                  field.setIsRecursiveReference(isRecursiveReference);
              }
  
              // 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, FieldMetadata::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, FieldMetadata::extractName);
          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 = fields.stream()
                      .map(FieldMetadata::getId)
                      .filter(Objects::nonNull)
                      .collect(toSet());
  
              // 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 Map<String, String> extractFieldIdlAnnotations(short fieldId, Collection<FieldMetadata> fields)
      {
          Set<Map<String, String>> idlAnnotationMaps =
                  fields.stream()
                          .map(field -> field == null ? null : field.getIdlAnnotations())
                          .filter(annotationMap -> annotationMap != null && !annotationMap.isEmpty())
                          .collect(toSet());
  
          if (idlAnnotationMaps.isEmpty()) {
              return ImmutableMap.of();
          }
  
          if (idlAnnotationMaps.size() > 1) {
              metadataErrors.addError("Thrift class '%s' field '%s' has conflicting IDL annotation maps", structName, fieldId);
          }
          return idlAnnotationMaps.iterator().next();
      }
  
      protected final boolean extractFieldIsRecursiveReference(short fieldId, Collection<FieldMetadata> fields)
      {
          Set<Boolean> isRecursiveReferences =
                  fields.stream()
                          .map(FieldMetadata::isRecursiveReference)
                          .filter(Objects::nonNull)
                          .collect(toSet());
  
          if (isRecursiveReferences.isEmpty()) {
              return false;
          }
  
          if (isRecursiveReferences.size() > 1) {
              metadataErrors.addError("Thrift class '%s' field '%s' has both isRecursiveReference=TRUE and isRecursiveReference=FALSE", structName, fieldId);
          }
          return isRecursiveReferences.iterator().next();
      }
  
      protected final boolean extractFieldIsLegacyId(short id, String fieldName, Collection<FieldMetadata> fields)
      {
          Set<Boolean> isLegacyIds = fields.stream()
                  .map(FieldMetadata::getThriftFieldIsLegacyId)
                  .filter(Optional::isPresent)
                  .map(Optional::get)
                  .collect(toSet());
  
          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 = fields.stream()
                  .map(FieldMetadata::getName)
                  .filter(Objects::nonNull)
                  .collect(toSet());
  
          if (!names.isEmpty()) {
              if (names.size() > 1) {
                  metadataErrors.addWarning("Thrift class %s field %s has multiple names %s", structName, id, names);
              }
              return names.iterator().next();
          }
  
          return fields.stream()
                  .map(FieldMetadata::extractName)
                  .filter(Objects::nonNull)
                  .findFirst()
                  .orElseThrow(() -> new IllegalArgumentException("Field name not found for ID: " + id));
      }
  
      protected final Requiredness extractFieldRequiredness(short fieldId, String fieldName, Collection<FieldMetadata> fields)
      {
          Set<Requiredness> requirednessValues = fields.stream()
                  .map(FieldMetadata::getRequiredness)
                  .filter(Objects::nonNull)
                  .filter(field -> field != Requiredness.UNSPECIFIED)
                  .collect(toSet());
  
          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<ThriftTypeReference> types = new HashSet<>();
              for (FieldMetadata field : fields) {
                  types.add(catalog.getFieldThriftTypeReference(field));
              }
              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()
      {
          Map<Short, List<FieldMetadata>> fieldsById = fields.stream().collect(groupingBy(FieldMetadata::getId));
          return fieldsById.values().stream()
                  .map(this::buildField)
                  .collect(toList());
      }
  
      protected final List<ThriftMethodInjection> buildMethodInjections()
      {
          return methodInjections.stream()
                  .map(injection -> new ThriftMethodInjection(injection.getMethod(), buildParameterInjections(injection.getParameters())))
                  .collect(toList());
      }
  
      protected static List<ThriftParameterInjection> buildParameterInjections(List<ParameterInjection> parameters)
      {
          return parameters.stream()
                  .map(injection -> new ThriftParameterInjection(
                          injection.getId(),
                          injection.getName(),
                          injection.getParameterIndex(),
                          injection.getJavaType()))
                  .collect(toList());
      }
  }
  