All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.twitter.util.validation.ScalaValidator.scala Maven / Gradle / Ivy

package com.twitter.util.validation

import com.twitter.util.Memoize
import com.twitter.util.logging.Logging
import com.twitter.util.reflect.{Classes, Annotations => ReflectAnnotations, Types => ReflectTypes}
import com.twitter.util.validation.cfg.ConstraintMapping
import com.twitter.util.validation.engine.MethodValidationResult
import com.twitter.util.validation.executable.ScalaExecutableValidator
import com.twitter.util.validation.internal.constraintvalidation.ConstraintValidatorContextFactory
import com.twitter.util.validation.internal.engine.ConstraintViolationFactory
import com.twitter.util.validation.internal.metadata.descriptor.{
  ConstraintDescriptorFactory,
  MethodValidationConstraintDescriptor
}
import com.twitter.util.validation.internal.{AnnotationFactory, Types, ValidationContext}
import com.twitter.util.validation.metadata._
import jakarta.validation._
import jakarta.validation.constraintvalidation.{SupportedValidationTarget, ValidationTarget}
import jakarta.validation.metadata.ConstraintDescriptor
import jakarta.validation.spi.ValidationProvider
import java.lang.annotation.Annotation
import java.lang.reflect.{Constructor, Executable, InvocationTargetException, Method, Parameter}
import java.util.Collections
import org.hibernate.validator.{HibernateValidator, HibernateValidatorConfiguration}
import org.hibernate.validator.internal.engine.constraintvalidation.ConstraintValidatorManager
import org.hibernate.validator.internal.engine.path.PathImpl
import org.hibernate.validator.internal.engine.{ValidatorFactoryImpl, ValidatorFactoryInspector}
import org.hibernate.validator.internal.metadata.aggregated.{
  CascadingMetaDataBuilder,
  ExecutableMetaData
}
import org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl
import org.hibernate.validator.internal.metadata.raw.ConstrainedElement.ConstrainedElementKind
import org.hibernate.validator.internal.metadata.raw.{
  ConfigurationSource,
  ConstrainedExecutable,
  ConstrainedParameter
}
import org.hibernate.validator.internal.properties.javabean.{
  ConstructorCallable,
  ExecutableCallable,
  MethodCallable
}
import org.json4s.reflect.{Reflector, ScalaType}
import scala.annotation.{tailrec, varargs}
import scala.collection.mutable
import scala.collection.mutable.ListBuffer
import scala.jdk.CollectionConverters._
import scala.util.control.NonFatal

object ScalaValidator {

  /** The size of the caffeine cache that is used to store reflection data on a validated case class. */
  private val DefaultDescriptorCacheSize: Long = 128

  case class Builder private[validation] (
    descriptorCacheSize: Long = DefaultDescriptorCacheSize,
    messageInterpolator: Option[MessageInterpolator] = None,
    constraintMappings: Set[ConstraintMapping] = Set.empty) {

    /**
     * Define the size of the cache that stores annotation data for validated case classes.
     */
    def withDescriptorCacheSize(size: Long): ScalaValidator.Builder =
      Builder(
        descriptorCacheSize = size,
        messageInterpolator = this.messageInterpolator,
        constraintMappings = this.constraintMappings
      )

    def withMessageInterpolator(
      messageInterpolator: MessageInterpolator
    ): ScalaValidator.Builder =
      Builder(
        descriptorCacheSize = this.descriptorCacheSize,
        messageInterpolator = Some(messageInterpolator),
        constraintMappings = this.constraintMappings
      )

    /**
     * Register a Set of [[ConstraintMapping]].
     *
     * @note Java users please see the version which takes a [[java.util.Set]]
     */
    def withConstraintMappings(
      constraintMappings: Set[ConstraintMapping]
    ): ScalaValidator.Builder =
      Builder(
        descriptorCacheSize = this.descriptorCacheSize,
        messageInterpolator = this.messageInterpolator,
        constraintMappings = constraintMappings
      )

    /**
     * Register a Set of [[ConstraintMapping]].
     *
     * @note Scala users please see the version which takes a [[Set]]
     */
    def withConstraintMappings(
      constraintMappings: java.util.Set[ConstraintMapping]
    ): ScalaValidator.Builder =
      Builder(
        descriptorCacheSize = this.descriptorCacheSize,
        messageInterpolator = this.messageInterpolator,
        constraintMappings = constraintMappings.asScala.toSet
      )

    /**
     * Register a [[ConstraintMapping]].
     */
    def withConstraintMapping(
      constraintMapping: ConstraintMapping
    ): ScalaValidator.Builder =
      Builder(
        descriptorCacheSize = this.descriptorCacheSize,
        messageInterpolator = this.messageInterpolator,
        constraintMappings = Set(constraintMapping)
      )

    def validator: ScalaValidator = {
      val configuration: HibernateValidatorConfiguration =
        Validation
          .byProvider[
            HibernateValidatorConfiguration,
            ValidationProvider[HibernateValidatorConfiguration]](
            classOf[HibernateValidator]
              .asInstanceOf[Class[ValidationProvider[HibernateValidatorConfiguration]]])
          .configure()

      // add user-configured message interpolator
      messageInterpolator.map { interpolator =>
        configuration.messageInterpolator(interpolator)
      }

      // add user-configured constraint mappings
      constraintMappings.foreach { constraintMapping =>
        val hibernateConstraintMapping: org.hibernate.validator.cfg.ConstraintMapping =
          configuration.createConstraintMapping()
        hibernateConstraintMapping
          .constraintDefinition(constraintMapping.annotationType.asInstanceOf[Class[Annotation]])
          .includeExistingValidators(constraintMapping.includeExistingValidators)
          .validatedBy(constraintMapping.constraintValidator
            .asInstanceOf[Class[_ <: ConstraintValidator[Annotation, _]]])
        configuration.addMapping(hibernateConstraintMapping)
      }

      val validatorFactory =
        configuration.buildValidatorFactory

      new ScalaValidator(
        this.descriptorCacheSize,
        new ValidatorFactoryInspector(validatorFactory.asInstanceOf[ValidatorFactoryImpl]),
        validatorFactory.getValidator
      )
    }
  }

  def builder: ScalaValidator.Builder = Builder()

  def apply(): ScalaValidator = ScalaValidator.builder.validator
}

class ScalaValidator private[validation] (
  cacheSize: Long,
  validatorFactory: ValidatorFactoryInspector,
  val underlying: Validator)
    extends ScalaExecutableValidator
    with Logging {

  /* Exposed for testing */
  private[validation] val descriptorFactory: DescriptorFactory =
    new DescriptorFactory(cacheSize, validatorFactory.constraintHelper)

  private[this] val constraintViolationFactory: ConstraintViolationFactory =
    new ConstraintViolationFactory(validatorFactory)

  private[this] val constraintDescriptorFactory: ConstraintDescriptorFactory =
    new ConstraintDescriptorFactory(validatorFactory)

  private[this] val constraintValidatorContextFactory: ConstraintValidatorContextFactory =
    new ConstraintValidatorContextFactory(validatorFactory)

  private[this] val constraintValidatorManager: ConstraintValidatorManager =
    validatorFactory.getConstraintCreationContext.getConstraintValidatorManager

  /** Close resources.*/
  def close(): Unit = {
    descriptorFactory.close()
    validatorFactory.close()
  }

  /**
   * Returns a [[CaseClassDescriptor]] object describing the given case class type constraints.
   * Descriptors describe constraints on a given class and any cascaded case class types.
   *
   * The returned object (and associated objects including CaseClassDescriptors) are immutable.
   *
   * @param clazz class or interface type evaluated
   * @tparam T the clazz type
   *
   * @return the [[CaseClassDescriptor]] for the specified class
   *
   * @see [[CaseClassDescriptor]]
   */
  def getConstraintsForClass[T](clazz: Class[T]): CaseClassDescriptor[T] =
    descriptorFactory.describe[T](clazz)

  /**
   * Returns a [[ExecutableDescriptor]] object describing a given [[Executable]].
   *
   * The returned object (and associated objects including ExecutableDescriptors) are immutable.
   *
   * @param executable the [[Executable]] to describe.
   *
   * @return the [[ExecutableDescriptor]] for the specified [[Executable]]
   *
   * @note the returned [[ExecutableDescriptor]] is NOT cached. It is up to the caller of this
   *       method to optimize any calls to this method.
   */
  private[twitter] def describeExecutable(
    executable: Executable,
    mixinClazz: Option[Class[_]]
  ): ExecutableDescriptor = descriptorFactory.describeExecutable(executable, mixinClazz)

  /**
   * Returns an array of [[MethodDescriptor]] instances describing the `@MethodValidation`-annotated
   * or otherwise constrained methods of the given case class type.
   *
   * @param clazz class or interface type evaluated
   *
   * @return an array of [[MethodDescriptor]] for `@MethodValidation`-annotated or otherwise
   *         constrained methods of the class
   *
   * @note the returned [[MethodDescriptor]] instances are NOT cached. It is up to the caller of this
   *       method to optimize any calls to this method.
   */
  private[twitter] def describeMethods(
    clazz: Class[_]
  ): Array[MethodDescriptor] = descriptorFactory.describeMethods(clazz)

  /**
   * Returns the set of Constraints which validate the given [[Annotation]] class.
   *
   * For instance if the constraint type is `Class[com.twitter.util.validation.constraints.CountryCode]`,
   * the returned set would contain an instance of the `ISO3166CountryCodeConstraintValidator`.
   *
   * @return the set of supporting constraint validators for a given [[Annotation]].
   * @note this method is memoized as it should only ever need to be calculated once for a given [[Class]].
   */
  val findConstraintValidators: Class[_ <: Annotation] => Set[ConstraintValidatorType] = Memoize {
    annotationType: Class[_ <: Annotation] =>
      // compute from constraint, otherwise compute from registry
      val validatedBy: Set[ConstraintValidatorType] =
        if (annotationType.isAnnotationPresent(classOf[Constraint])) {
          val constraintAnnotation = annotationType.getAnnotation(classOf[Constraint])
          constraintAnnotation
            .validatedBy().map { clazz =>
              val instance = validatorFactory.constraintValidatorFactory.getInstance(clazz)
              if (instance == null)
                throw new ValidationException(
                  s"Constraint factory returned null when trying to create instance of $clazz.")
              instance.asInstanceOf[ConstraintValidatorType]
            }.toSet
        } else Set.empty
      if (validatedBy.isEmpty) {
        validatorFactory.constraintHelper
          .getAllValidatorDescriptors(annotationType).asScala
          .map(_.newInstance(validatorFactory.constraintValidatorFactory)).toSet
      } else validatedBy
  }

  /**
   * Checks whether the specified [[Annotation]] is a valid constraint constraint. A constraint
   * constraint has to fulfill the following conditions:
   *
   *   - Must be annotated with [[jakarta.validation.Constraint]]
   *   - Define a `message` parameter
   *   - Define a `groups` parameter
   *   - Define a `payload` parameter
   *
   * @param annotation The [[Annotation]] to test.
   *
   * @return true if the constraint fulfills the above conditions, false otherwise.
   */
  def isConstraintAnnotation(annotation: Annotation): Boolean =
    isConstraintAnnotation(annotation.annotationType())

  /**
   * Checks whether the specified [[Annotation]] clazz is a valid constraint constraint. A constraint
   * constraint has to fulfill the following conditions:
   *
   *   - Must be annotated with [[jakarta.validation.Constraint]]
   *   - Define a `message` parameter
   *   - Define a `groups` parameter
   *   - Define a `payload` parameter
   *
   * @return true if the constraint fulfills the above conditions, false otherwise.
   * @note this method is memoized as it should only ever need to be calculated once for a given [[Class]].
   */
  val isConstraintAnnotation: Class[_ <: Annotation] => Boolean = Memoize {
    annotationType: Class[_ <: Annotation] =>
      validatorFactory.constraintHelper.isConstraintAnnotation(annotationType)
  }

  /**
   * Validates all constraint constraints on an object.
   *
   * @param obj the object to validate.
   *
   * @throws ValidationException      - if any constraints produce a violation.
   * @throws IllegalArgumentException - if object is null.
   */
  @throws[ValidationException]
  def verify(obj: Any, groups: Class[_]*): Unit = {
    val invalidResult = validate(obj, groups: _*)
    if (invalidResult.nonEmpty)
      throw constraintViolationFactory.newConstraintViolationException(invalidResult)
  }

  /**
   * Validates all constraint constraints on an object.
   *
   * @param obj    the object to validate.
   * @param groups the list of groups targeted for validation (defaults to Default).
   * @tparam T the type of the object to validate.
   *
   * @return constraint violations or an empty set if none
   *
   * @throws IllegalArgumentException - if object is null.
   */
  def validate[T](
    obj: T,
    groups: Class[_]*
  ): Set[ConstraintViolation[T]] = {
    if (obj == null) throw new IllegalArgumentException("value must not be null.")
    val context = ValidationContext[T](None, None, None, None, PathImpl.createRootPath())
    validate[T](
      maybeDescriptor = None,
      context = context,
      value = obj,
      groups = groups
    )
  }

  /**
   * Validates all constraint constraints on the `fieldName` field of the given class `beanType`
   * if the `fieldName` field value were `value`.
   *
   * ConstraintViolation objects return `null` for ConstraintViolation.getRootBean() and
   * ConstraintViolation.getLeafBean().
   *
   * ==Usage==
   * Validate value of "" for the "id" field in case class `MyClass` (enabling group "Checks"):
   * {{{
   *   case class MyCaseClass(@NotEmpty(groups = classOf[Checks]) id)
   *   validator.validateFieldValue(classOf[MyCaseClass], "id", "", Seq(classOf[Checks]))
   * }}}
   *
   * @param beanType     the case class type.
   * @param propertyName field to validate.
   * @param value        field value to validate.
   * @param groups       the list of groups targeted for validation (defaults to Default).
   * @tparam T the type of the object to validate
   *
   * @return constraint violations or an empty set if none
   *
   * @throws IllegalArgumentException - if beanType is null, if fieldName is null, empty or not a valid object property.
   */
  def validateValue[T](
    beanType: Class[T],
    propertyName: String,
    value: Any,
    groups: Class[_]*
  ): Set[ConstraintViolation[T]] = {
    if (beanType == null) throw new IllegalArgumentException("beanType must not be null.")
    val descriptor = getConstraintsForClass(beanType)
    validateValue[T](descriptor, propertyName, value, groups: _*)
  }

  /**
   * Validates all constraint constraints on the `fieldName` field of the class described
   * by the given [[CaseClassDescriptor]] if the `fieldName` field value were `value`.
   *
   * @param descriptor   the [[CaseClassDescriptor]] of the described class.
   * @param propertyName field to validate.
   * @param value        field value to validate.
   * @param groups       the list of groups targeted for validation (defaults to Default).
   * @tparam T the type of the object to validate
   *
   * @return constraint violations or an empty set if none
   *
   * @throws IllegalArgumentException - if fieldName is null, empty or not a valid object property.
   */
  def validateValue[T](
    descriptor: CaseClassDescriptor[T],
    propertyName: String,
    value: Any,
    groups: Class[_]*
  ): Set[ConstraintViolation[T]] = {
    if (descriptor == null)
      throw new IllegalArgumentException("descriptor must not be null.")
    if (propertyName == null || propertyName.isEmpty)
      throw new IllegalArgumentException("fieldName must not be null or empty.")

    val mangledName = DescriptorFactory.mangleName(propertyName)
    descriptor.members.get(mangledName) match {
      case Some(propertyDescriptor) =>
        val context = ValidationContext(
          Some(propertyName),
          Some(descriptor.clazz),
          None,
          None,
          PathImpl.createRootPath())
        validate(
          maybeDescriptor = Some(propertyDescriptor),
          context = context,
          value = value,
          groups = groups
        )
      case _ =>
        throw new IllegalArgumentException(s"$propertyName is not a field of ${descriptor.clazz}.")
    }
  }

  /**
   * Validates all constraint constraints on the `fieldName` field of the given object.
   *
   * ==Usage==
   * Validate the "id" field in case class `MyClass` (enabling the validation group "Checks"):
   * {{{
   *   case class MyClass(@NotEmpty(groups = classOf[Checks]) id)
   *   val i = MyClass("")
   *   validator.validateProperty(i, "id", Seq(classOf[Checks]))
   * }}}
   *
   * @param obj          object to validate.
   * @param propertyName property to validate (used in error reporting).
   * @param groups       the list of groups targeted for validation (defaults to Default).
   * @tparam T the type of the object to validate.
   *
   * @return constraint violations or an empty set if none.
   *
   * @throws IllegalArgumentException - if object is null, if fieldName is null, empty or not a valid object property.
   */
  def validateProperty[T](
    obj: T,
    propertyName: String,
    groups: Class[_]*
  ): Set[ConstraintViolation[T]] = {
    if (obj == null) throw new IllegalArgumentException("obj must not be null.")
    if (propertyName == null || propertyName.isEmpty)
      throw new IllegalArgumentException("fieldName must not be null or empty.")

    val caseClassDescriptor = getConstraintsForClass(obj.getClass)

    val mangledName = DescriptorFactory.mangleName(propertyName)
    caseClassDescriptor.members.get(mangledName) match {
      case Some(_) =>
        val context = ValidationContext(
          Some(propertyName),
          Some(obj.getClass.asInstanceOf[Class[T]]),
          Some(obj),
          Some(obj),
          PathImpl.createRootPath())
        validate(
          maybeDescriptor = Some(caseClassDescriptor),
          context = context,
          value = obj,
          groups = groups
        )
      case _ =>
        throw new IllegalArgumentException(
          s"$propertyName is not a field of ${caseClassDescriptor.clazz}.")
    }
  }

  /** Returns the contract for validating parameters and return values of methods and constructors. */
  def forExecutables: ScalaExecutableValidator = this

  /**
   * Returns an instance of the specified type allowing access to provider-specific APIs.
   *
   * If the Jakarta Bean Validation provider implementation does not support the specified class, ValidationException is thrown.
   *
   * @param clazz the class of the object to be returned
   * @tparam U the type of the object to be returned
   *
   * @return an instance of the specified class
   *
   * @throws ValidationException - if the provider does not support the call.
   */
  @throws[ValidationException]
  def unwrap[U](clazz: Class[U]): U = {
    // If this were an implementation that implemented the specification
    // interface, this is where users would be able to cast the validator from the
    // interface type into our specific implementation type in order to call methods
    // only available on the implementation. However, since the specification uses Java
    // collection types we do not directly implement and instead expose our feature-compatible
    // ScalaValidator directly. We try to remain true to the spirit of the specification
    // interface and thus implement unwrap but only to return this implementation.
    if (clazz.isAssignableFrom(classOf[ScalaValidator])) {
      this.asInstanceOf[U]
    } else if (clazz.isAssignableFrom(classOf[ScalaExecutableValidator])) {
      this.asInstanceOf[U]
    } else {
      throw new ValidationException(s"Type ${clazz.getName} not supported for unwrapping.")
    }
  }

  /* Executable Validator Methods */

  /** @inheritdoc */
  def validateMethods[T](
    obj: T,
    groups: Class[_]*
  ): Set[ConstraintViolation[T]] = {
    if (obj == null)
      throw new IllegalArgumentException("The object to be validated must not be null.")
    val caseClassDescriptor = getConstraintsForClass(obj.getClass)
    validateMethods[T](
      rootClazz = Some(obj.getClass.asInstanceOf[Class[T]]),
      root = Some(obj),
      methods = caseClassDescriptor.methods,
      obj = obj,
      groups = groups
    )
  }

  /** @inheritdoc */
  def validateMethod[T](
    obj: T,
    method: Method,
    groups: Class[_]*
  ): Set[ConstraintViolation[T]] = {
    if (obj == null)
      throw new IllegalArgumentException("The object to be validated must not be null.")
    if (method == null)
      throw new IllegalArgumentException("The method to be validated must not be null.")
    val caseClassDescriptor = getConstraintsForClass(obj.getClass)
    caseClassDescriptor.methods.find(_.method.getName == method.getName) match {
      case Some(methodDescriptor) =>
        validateMethods[T](
          rootClazz = Some(obj.getClass.asInstanceOf[Class[T]]),
          root = Some(obj),
          methods = Array(methodDescriptor),
          obj = obj,
          groups = groups
        )
      case _ =>
        throw new IllegalArgumentException(
          s"${method.getName} is not method of ${caseClassDescriptor.clazz}.")
    }
  }

  /** @inheritdoc */
  def validateParameters[T](
    obj: T,
    method: Method,
    parameterValues: Array[Any],
    groups: Class[_]*
  ): Set[ConstraintViolation[T]] = {
    if (obj == null)
      throw new IllegalArgumentException("The object to be validated must not be null.")
    if (method == null)
      throw new IllegalArgumentException("The method to be validated must not be null.")

    val caseClassDescriptor = getConstraintsForClass(obj.getClass)
    caseClassDescriptor.methods.find(_.method.getName == method.getName) match {
      case Some(methodDescriptor: ExecutableDescriptor) =>
        validateExecutableParameters[T](
          executableDescriptor = methodDescriptor,
          root = Some(obj),
          leaf = Some(obj),
          parameterValues = parameterValues,
          parameterNames = None,
          groups = groups
        )
      case _ =>
        throw new IllegalArgumentException(
          s"${method.getName} is not method of ${caseClassDescriptor.clazz}.")
    }
  }

  /** @inheritdoc */
  def validateReturnValue[T](
    obj: T,
    method: Method,
    returnValue: Any,
    groups: Class[_]*
  ): Set[ConstraintViolation[T]] = underlying
    .forExecutables().validateReturnValue(
      obj,
      method,
      returnValue,
      groups: _*
    ).asScala.toSet

  /** @inheritdoc */
  def validateConstructorParameters[T](
    constructor: Constructor[T],
    parameterValues: Array[Any],
    groups: Class[_]*
  ): Set[ConstraintViolation[T]] = {
    if (constructor == null)
      throw new IllegalArgumentException("The executable to be validated must not be null.")
    if (parameterValues == null)
      throw new IllegalArgumentException("The constructor parameter array cannot not be null.")

    val parameterCount = constructor.getParameterCount
    val parameterValuesLength = parameterValues.length
    if (parameterCount != parameterValuesLength)
      throw new ValidationException(
        s"Wrong number of parameters. Method or constructor $constructor expects $parameterCount parameters, but got $parameterValuesLength.")

    validateExecutableParameters[T](
      executableDescriptor = descriptorFactory.describe[T](constructor),
      root = None,
      leaf = None,
      parameterValues = parameterValues,
      parameterNames = None,
      groups = groups
    )
  }

  /** @inheritdoc */
  def validateMethodParameters[T](
    method: Method,
    parameterValues: Array[Any],
    groups: Class[_]*
  ): Set[ConstraintViolation[T]] = {
    if (method == null)
      throw new IllegalArgumentException("The executable to be validated must not be null.")
    if (parameterValues == null)
      throw new IllegalArgumentException("The method parameter array cannot not be null.")

    val parameterCount = method.getParameterCount
    val parameterValuesLength = parameterValues.length
    if (parameterCount != parameterValuesLength)
      throw new ValidationException(
        s"Wrong number of parameters. Method or constructor $method expects $parameterCount parameters, but got $parameterValuesLength.")

    descriptorFactory.describe(method) match {
      case Some(descriptor: ExecutableDescriptor) =>
        validateExecutableParameters[T](
          executableDescriptor = descriptor,
          root = None,
          leaf = None,
          parameterValues = parameterValues,
          parameterNames = None,
          groups = groups
        )
      case _ => Set.empty[ConstraintViolation[T]]
    }
  }

  /** @inheritdoc */
  def validateConstructorReturnValue[T](
    constructor: Constructor[T],
    createdObject: T,
    groups: Class[_]*
  ): Set[ConstraintViolation[T]] =
    underlying
      .forExecutables().validateConstructorReturnValue(
        constructor,
        createdObject,
        groups: _*).asScala.toSet

  /* Private */

  /** @inheritdoc */
  private[twitter] def validateExecutableParameters[T](
    executable: ExecutableDescriptor,
    parameterValues: Array[Any],
    parameterNames: Array[String],
    groups: Class[_]*
  ): Set[ConstraintViolation[T]] = {
    if (executable == null)
      throw new IllegalArgumentException("The executable to be validated must not be null.")
    if (parameterValues == null)
      throw new IllegalArgumentException("The constructor parameter array cannot not be null.")

    val parameterCount = executable.executable.getParameterCount
    val parameterValuesLength = parameterValues.length
    if (parameterCount != parameterValuesLength)
      throw new ValidationException(
        s"Wrong number of parameters. Method or constructor $executable expects $parameterCount parameters, but got $parameterValuesLength.")

    validateExecutableParameters[T](
      executableDescriptor = executable,
      root = None,
      leaf = None,
      parameterValues = parameterValues,
      parameterNames = Some(parameterNames),
      groups = groups
    )
  }

  /** @inheritdoc */
  private[twitter] def validateMethods[T](
    methods: Array[MethodDescriptor],
    obj: T,
    groups: Class[_]*
  ): Set[ConstraintViolation[T]] = {
    validateMethods[T](
      rootClazz = Some(obj.getClass.asInstanceOf[Class[T]]),
      root = Some(obj),
      methods = methods,
      obj = obj,
      groups = groups
    )
  }

  // note: Prefer while loop over Scala for loop for better performance. The scala for loop
  // performance is optimized in 2.13.0 if we enable scalac: https://github.com/scala/bug/issues/1338

  /**
   * Evaluates all known [[ConstraintValidator]] instances supporting the constraints represented by the
   * given mapping of [[Annotation]] to constraint attributes placed on a field `fieldName` if
   * the `fieldName` field value were `value`. The `fieldName` is used in error reporting (as the
   * the resulting property path for any resulting [[ConstraintViolation]]).
   *
   * E.g., the given mapping could be Map(jakarta.validation.constraint.Size -> Map("min" -> 0, "max" -> 1000))
   *
   * ConstraintViolation objects return `null` for ConstraintViolation.getRootBeanClass(),
   * ConstraintViolation.getRootBean() and ConstraintViolation.getLeafBean().
   *
   * @param constraints  the mapping of constraint [[Annotation]] to attributes to evaluate
   * @param fieldName    field to validate (used in error reporting).
   * @param value        field value to validate.
   * @param groups       the list of groups targeted for validation (defaults to Default).
   *
   * @return constraint violations or an empty set if none
   *
   * @throws IllegalArgumentException - if fieldName is null, empty or not a valid object property.
   *
   * @note Scala users should prefer the version which takes a [[Map]] of constraints and
   *       a [[Seq]] of groups.
   */
  @varargs
  private[twitter] def validateFieldValue(
    constraints: java.util.Map[Class[_ <: Annotation], java.util.Map[String, Any]],
    fieldName: String,
    value: Any,
    groups: Class[_]*
  ): java.util.Set[ConstraintViolation[Any]] = {
    if (fieldName == null || fieldName.isEmpty)
      throw new IllegalArgumentException("fieldName must not be null or empty.")
    val annotations = constraints.asScala.map {
      case (constraintAnnotationType, attributes)
          if isConstraintAnnotation(constraintAnnotationType) =>
        AnnotationFactory.newInstance(constraintAnnotationType, attributes.asScala.toMap)
    }
    validateFieldValue(fieldName, annotations.toArray, value, groups.toSeq).asJava
  }

  /**
   * Evaluates all known [[ConstraintValidator]] instances supporting the constraints represented by the
   * given mapping of [[Annotation]] to constraint attributes placed on a field `fieldName` if
   * the `fieldName` field value were `value`. The `fieldName` is used in error reporting (as the
   * the resulting property path for any resulting [[ConstraintViolation]]).
   *
   * E.g., the given mapping could be Map(jakarta.validation.constraint.Size -> Map("min" -> 0, "max" -> 1000))
   *
   * ConstraintViolation objects return `null` for ConstraintViolation.getRootBeanClass(),
   * ConstraintViolation.getRootBean() and ConstraintViolation.getLeafBean().
   *
   * @param constraints  the mapping of constraint [[Annotation]] to attributes to evaluate
   * @param fieldName    field to validate (used in error reporting).
   * @param value        field value to validate.
   * @param groups       the list of groups targeted for validation (defaults to Default).
   *
   * @return constraint violations or an empty set if none
   *
   * @throws IllegalArgumentException - if fieldName is null, empty or not a valid object property.
   *
   * @note Java users should prefer the version which takes a [[java.util.Map]] of constraints and
   *       a [[java.util.List]] of groups.
   */
  private[twitter] def validateFieldValue(
    constraints: Map[Class[_ <: Annotation], Map[String, Any]],
    fieldName: String,
    value: Any,
    groups: Class[_]*
  ): Set[ConstraintViolation[Any]] = {
    if (fieldName == null || fieldName.isEmpty)
      throw new IllegalArgumentException("fieldName must not be null or empty.")

    val annotations = constraints.map {
      case (constraintAnnotationType, attributes) =>
        AnnotationFactory.newInstance(constraintAnnotationType, attributes)
    }

    validateFieldValue(fieldName, annotations.toArray, value, groups)
  }

  /** Validate the given fieldName with the given constraint constraints, value, and groups. */
  private[this] def validateFieldValue(
    fieldName: String,
    constraints: Array[Annotation],
    value: Any,
    groups: Seq[Class[_]]
  ): Set[ConstraintViolation[Any]] =
    if (constraints.nonEmpty) {
      val results = new mutable.ListBuffer[ConstraintViolation[Any]]()
      var index = 0
      val length = constraints.length
      while (index < length) {
        val annotation = constraints(index)
        val path = PathImpl.createRootPath
        path.addPropertyNode(fieldName)
        val clazz = classOf[Any]
        val scalaType = Reflector.scalaTypeOf(value.getClass)
        val context = ValidationContext(Some(fieldName), Some(clazz), None, None, path)

        results.appendAll(
          isValid[Any](
            context = context,
            constraint = annotation,
            scalaType = scalaType,
            value = value,
            groups = groups
          )
        )
        index += 1
      }
      results.toSet
    } else Set.empty[ConstraintViolation[Any]]

  // https://beanvalidation.org/2.0/spec/#constraintsdefinitionimplementation-validationimplementation
  // From the documentation:
  //
  // While not mandatory, it is considered a good practice to split the core constraint
  // validation from the "not null" constraint validation (for example, an @Email constraint
  // should return "true" on a `null` object, i.e. it will not also assert the @NotNull validation).
  // A "null" can have multiple meanings but is commonly used to express that a value does
  // not make sense, is not available or is simply unknown. Those constraints on the
  // value are orthogonal in most cases to other constraints. For example a String,
  // if present, must be an email but can be null. Separating both concerns is a good
  // practice.
  //
  // Thus, we "ignore" any value that is null and not annotated with @NotNull.
  private[this] def ignorable(value: Any, annotation: Annotation): Boolean = value match {
    case null if !ReflectAnnotations.equals[jakarta.validation.constraints.NotNull](annotation) =>
      true
    case _ => false
  }

  /** Validate Executable field-level parameters (including cross-field parameters) */
  private[this] def validateExecutableParameters[T](
    executableDescriptor: ExecutableDescriptor,
    root: Option[T],
    leaf: Option[Any],
    parameterValues: Array[Any],
    parameterNames: Option[Array[String]],
    groups: Seq[Class[_]]
  ): Set[ConstraintViolation[T]] = {
    if (executableDescriptor.members.size != parameterValues.length)
      throw new IllegalArgumentException(
        s"Invalid number of arguments for method ${executableDescriptor.executable.getName}.")
    val results = new mutable.ListBuffer[ConstraintViolation[T]]()
    val parameters: Array[Parameter] = executableDescriptor.executable.getParameters
    val parameterNamesList: Array[String] =
      (parameterNames match {
        case Some(names) =>
          names
        case _ =>
          getExecutableParameterNames(executableDescriptor.executable)
      }).map { maybeMangled =>
        DescriptorFactory.unmangleName(
          maybeMangled
        ) // parameter names are encoded since Scala 2.13.5
      }

    // executable parameter constraints
    var index = 0
    val parametersLength = parameterNamesList.length
    while (index < parametersLength) {
      val parameter = parameters(index)
      val parameterValue = parameterValues(index)
      // only for property path use to affect error reporting
      val parameterName = parameterNamesList(index)

      val parameterPath =
        PathImpl.createPathForExecutable(getExecutableMetaData(executableDescriptor.executable))
      parameterPath.addParameterNode(parameterName, index)

      val propertyDescriptor = executableDescriptor.members(parameter.getName)
      val validateFieldContext =
        ValidationContext[T](
          Some(parameterName),
          Some(executableDescriptor.executable.getDeclaringClass.asInstanceOf[Class[T]]),
          root,
          leaf,
          parameterPath)
      val parameterViolations = validateField[T](
        validateFieldContext,
        propertyDescriptor,
        parameterValue,
        groups
      )
      if (parameterViolations.nonEmpty) results.appendAll(parameterViolations)

      index += 1
    }
    results.toSet

    // executable cross-parameter constraints
    val crossParameterPath = PathImpl
      .createPathForExecutable(getExecutableMetaData(executableDescriptor.executable))
    crossParameterPath.addCrossParameterNode()
    val constraints = executableDescriptor.annotations
    index = 0
    val constraintsLength = constraints.length
    while (index < constraintsLength) {
      val constraint = constraints(index)
      val scalaType = Reflector.scalaTypeOf(parameterValues.getClass)

      val validators =
        findConstraintValidators(constraint.annotationType())
          .filter(parametersValidationTargetFilter)

      val validatorsIterator = validators.iterator
      while (validatorsIterator.hasNext) {
        val validator = validatorsIterator.next().asInstanceOf[ConstraintValidator[Annotation, _]]
        val constraintDescriptor = constraintDescriptorFactory
          .newConstraintDescriptor(
            name = executableDescriptor.executable.getName,
            clazz = scalaType.erasure,
            declaringClazz = executableDescriptor.executable.getDeclaringClass,
            annotation = constraint,
            constrainedElementKind = ConstrainedElementKind.METHOD
          )
        if (groupsEnabled(constraintDescriptor, groups)) {
          // initialize validator
          validator.initialize(constraint)
          val validateCrossParameterContext =
            ValidationContext[T](
              None,
              Some(executableDescriptor.executable.getDeclaringClass.asInstanceOf[Class[T]]),
              root,
              leaf,
              crossParameterPath)
          results.appendAll(
            isValid(
              context = validateCrossParameterContext,
              constraint = constraint,
              scalaType = scalaType,
              value = parameterValues,
              constraintDescriptorOption = Some(constraintDescriptor),
              validatorOption = Some(validator.asInstanceOf[ConstraintValidator[Annotation, Any]]),
              groups = groups
            )
          )
        }
      }
      index += 1
    }
    results.toSet
  }

  /** Validate method-level constraints, i.e., @MethodValidation annotated methods */
  private[this] def validateMethods[T](
    rootClazz: Option[Class[T]],
    root: Option[T],
    methods: Array[MethodDescriptor],
    obj: T,
    groups: Seq[Class[_]]
  ): Set[ConstraintViolation[T]] = {
    val results = new mutable.ListBuffer[ConstraintViolation[T]]()
    if (methods.nonEmpty) {
      var index = 0
      val length = methods.length
      while (index < length) {
        val methodDescriptor = methods(index)
        val validateMethodContext =
          ValidationContext(None, rootClazz, root, Some(obj), PathImpl.createRootPath())
        results.appendAll(
          validateMethod(validateMethodContext, methodDescriptor, obj, groups)
        )
        index += 1
      }
    }
    results.toSet
  }

  // BEGIN: Recursive validation methods -----------------------------------------------------------

  /** Validate field-level constraints */
  private[this] def validateField[T](
    context: ValidationContext[T],
    propertyDescriptor: PropertyDescriptor,
    fieldValue: Any,
    groups: Seq[Class[_]]
  ): Set[ConstraintViolation[T]] = {
    val constraints: Array[Annotation] = propertyDescriptor.annotations
    val results = new mutable.ListBuffer[ConstraintViolation[T]]()
    var index = 0
    val length = constraints.length
    while (index < length) {
      val constraint = constraints(index)
      results.appendAll(
        isValid[T](
          context,
          constraint = constraint,
          scalaType = propertyDescriptor.scalaType,
          value = fieldValue,
          groups = groups
        )
      )
      index += 1
    }

    // Cannot cascade a null or None value
    fieldValue match {
      case null | None => // do nothing
      case _ =>
        if (propertyDescriptor.isCascaded) {
          results.appendAll(
            validateCascadedProperty[T](context, propertyDescriptor, fieldValue, groups)
          )
        }
    }

    if (results.isEmpty) Set.empty[ConstraintViolation[T]]
    else results.toSet
  }

  /** Validate cascaded field-level properties */
  private[this] def validateCascadedProperty[T](
    context: ValidationContext[T],
    propertyDescriptor: PropertyDescriptor,
    clazzInstance: Any,
    groups: Seq[Class[_]]
  ): Set[ConstraintViolation[T]] = propertyDescriptor.cascadedScalaType match {
    case Some(cascadedScalaType) =>
      // Update the found value with the current landscape, for example, the retrieved case class
      // descriptor may be scala type Foo, but this property is a Seq[Foo] or an Option[Foo] and
      // we want to carry that property typing through.
      val caseClassDescriptor =
        descriptorFactory
          .describe[T](cascadedScalaType.erasure.asInstanceOf[Class[T]])
          .copy(scalaType = propertyDescriptor.scalaType)

      val caseClassPath = PathImpl.createCopy(context.path)
      caseClassDescriptor.scalaType match {
        case argType if argType.isCollection =>
          val results = mutable.ListBuffer[ConstraintViolation[T]]()
          val collectionValue: Iterable[_] = clazzInstance.asInstanceOf[Iterable[_]]
          val collectionValueIterator = collectionValue.iterator
          var index = 0
          while (collectionValueIterator.hasNext) {
            val instanceValue = collectionValueIterator.next()
            // apply the index to the parent path, then use this to recompute paths of members and methods
            val indexedPropertyPath = {
              val indexedPath = PathImpl.createCopyWithoutLeafNode(caseClassPath)
              indexedPath.addPropertyNode(
                s"${caseClassPath.getLeafNode.asString()}[${index.toString}]")
              indexedPath
            }
            val violations = validate[T](
              maybeDescriptor = Some(
                caseClassDescriptor
                  .copy(
                    scalaType = caseClassDescriptor.scalaType.typeArgs.head,
                    members = caseClassDescriptor.members,
                    methods = caseClassDescriptor.methods
                  )
              ),
              context = context.copy(path = indexedPropertyPath),
              value = instanceValue,
              groups = groups
            )
            if (violations.nonEmpty) results.appendAll(violations)
            index += 1
          }
          if (results.nonEmpty) results.toSet
          else Set.empty[ConstraintViolation[T]]
        case argType if argType.isOption =>
          val optionValue = clazzInstance.asInstanceOf[Option[_]]
          validate[T](
            maybeDescriptor = Some(
              caseClassDescriptor.copy(scalaType = caseClassDescriptor.scalaType.typeArgs.head)),
            context = context.copy(path = caseClassPath),
            value = optionValue,
            groups = groups
          )
        case _ =>
          validate[T](
            maybeDescriptor = Some(caseClassDescriptor),
            context = context.copy(path = caseClassPath),
            value = clazzInstance,
            groups = groups
          )
      }
    case _ => Set.empty[ConstraintViolation[T]]
  }

  /** Validate @MethodValidation annotated methods */
  private[this] def validateMethod[T](
    context: ValidationContext[T],
    methodDescriptor: MethodDescriptor,
    clazzInstance: Any,
    groups: Seq[Class[_]]
  ): Set[ConstraintViolation[T]] =
    ReflectAnnotations.findAnnotation[MethodValidation](methodDescriptor.annotations) match {
      case Some(annotation) =>
        // if we're unable to invoke the method, we want this propagate
        val constraintDescriptor = new MethodValidationConstraintDescriptor(annotation)
        if (groupsEnabled(constraintDescriptor, groups)) {
          try {
            val result =
              methodDescriptor.method.invoke(clazzInstance).asInstanceOf[MethodValidationResult]
            val pathWithMethodName = PathImpl.createCopy(context.path)
            pathWithMethodName.addPropertyNode(methodDescriptor.method.getName)
            parseMethodValidationFailures[T](
              rootClazz = context.rootClazz,
              root = context.root,
              leaf = Some(clazzInstance),
              path = pathWithMethodName,
              annotation = annotation.asInstanceOf[MethodValidation],
              method = methodDescriptor.method,
              result = result,
              value = clazzInstance,
              constraintDescriptor
            )
          } catch {
            case e: InvocationTargetException if e.getCause != null =>
              throw e.getCause
          }
        } else Set.empty[ConstraintViolation[T]]
      case _ => Set.empty[ConstraintViolation[T]]
    }

  /** Validate class-level constraints */
  private[this] def validateClazz[T](
    context: ValidationContext[T],
    caseClassDescriptor: CaseClassDescriptor[T],
    clazzInstance: Any,
    groups: Seq[Class[_]]
  ): Set[ConstraintViolation[T]] = {
    val constraints = caseClassDescriptor.annotations
    if (constraints.nonEmpty) {
      val results = new mutable.ListBuffer[ConstraintViolation[T]]()
      var index = 0
      val length = constraints.length
      while (index < length) {
        val annotation = constraints(index)
        results.appendAll(
          isValid[T](
            context = context,
            constraint = annotation,
            scalaType = caseClassDescriptor.scalaType,
            value = clazzInstance,
            groups = groups
          )
        )
        index += 1
      }
      results.toSet
    } else Set.empty[ConstraintViolation[T]]
  }

  /** Recursively validate full case class */
  @tailrec
  private[this] def validate[T](
    maybeDescriptor: Option[Descriptor],
    context: ValidationContext[T],
    value: Any,
    groups: Seq[Class[_]]
  ): Set[ConstraintViolation[T]] = maybeDescriptor match {
    case Some(descriptor) =>
      descriptor match {
        case p: PropertyDescriptor =>
          val propertyPath = PathImpl.createCopy(context.path)
          context.fieldName.map(propertyPath.addPropertyNode)
          // validateField recurses back through validate for cascaded properties
          validateField[T](context.copy(path = propertyPath), p, value, groups)
        case c: CaseClassDescriptor[_] =>
          val memberViolations = {
            val results = new mutable.ListBuffer[ConstraintViolation[T]]()
            if (c.members.nonEmpty) {
              val keys: Iterable[String] = c.members.keys
              val keyIterator = keys.iterator
              while (keyIterator.hasNext) {
                val name = keyIterator.next()
                val propertyDescriptor = c.members(name)
                val propertyPath = PathImpl.createCopy(context.path)
                propertyPath.addPropertyNode(DescriptorFactory.unmangleName(name))
                // validateField recurses back through validate here for cascaded properties
                val fieldResults =
                  validateField[T](
                    context.copy(fieldName = Some(name), path = propertyPath),
                    propertyDescriptor,
                    findFieldValue(value, c.clazz, name),
                    groups)
                if (fieldResults.nonEmpty) results.appendAll(fieldResults)
              }
            }
            results.toSet
          }
          val methodViolations = {
            val results = new ListBuffer[ConstraintViolation[T]]()
            if (c.methods.nonEmpty) {
              var index = 0
              val length = c.methods.length
              while (index < length) {
                val methodResults = value match {
                  case null | None =>
                    Set.empty
                  case Some(obj) =>
                    validateMethod[T](context.copy(fieldName = None), c.methods(index), obj, groups)
                  case _ =>
                    validateMethod[T](
                      context.copy(fieldName = None),
                      c.methods(index),
                      value,
                      groups)
                }

                if (methodResults.nonEmpty) results.appendAll(methodResults)
                index += 1
              }
            }
            results.toSet
          }
          val clazzViolations =
            validateClazz[T](context, c.asInstanceOf[CaseClassDescriptor[T]], value, groups)

          // put them all together
          memberViolations ++ methodViolations ++ clazzViolations
      }
    case _ =>
      val clazz: Class[T] = value.getClass.asInstanceOf[Class[T]]
      if (ReflectTypes.notCaseClass(clazz))
        throw new ValidationException(s"$clazz is not a valid case class.")
      val caseClassDescriptor: CaseClassDescriptor[T] = descriptorFactory.describe(clazz)
      val context = ValidationContext(
        fieldName = None,
        rootClazz = Some(clazz),
        root = Some(value.asInstanceOf[T]),
        leaf = Some(value),
        path = PathImpl.createRootPath())
      validate[T](Some(caseClassDescriptor), context, value, groups)
  }

  // END: Recursive validation methods -------------------------------------------------------------

  @tailrec
  private[this] def findFieldValue[T](
    obj: Any,
    clazz: Class[T],
    name: String
  ): Any = obj match {
    case null | None =>
      obj
    case Some(v) =>
      findFieldValue(v, clazz, name)
    case _ =>
      try {
        val field = clazz.getDeclaredField(name)
        field.setAccessible(true)
        Option(field.get(obj)).orNull.asInstanceOf[T]
      } catch {
        case NonFatal(t) =>
          throw new ValidationException(t)
      }
  }

  private[this] def isValid[T](
    context: ValidationContext[T],
    constraint: Annotation,
    scalaType: ScalaType,
    value: Any,
    groups: Seq[Class[_]]
  ): Set[ConstraintViolation[T]] =
    isValid(context, constraint, scalaType, value, None, None, groups)

  private[this] def isValid[T](
    context: ValidationContext[T],
    constraint: Annotation,
    scalaType: ScalaType,
    value: Any,
    constraintDescriptorOption: Option[ConstraintDescriptorImpl[Annotation]],
    validatorOption: Option[ConstraintValidator[Annotation, Any]],
    groups: Seq[Class[_]]
  ): Set[ConstraintViolation[T]] = value match {
    case _: Option[_] =>
      isValidOption(
        context,
        constraint,
        scalaType,
        value,
        constraintDescriptorOption,
        validatorOption,
        groups)
    case _ =>
      val constraintDescriptor = constraintDescriptorOption match {
        case Some(descriptor) =>
          descriptor
        case _ =>
          constraintDescriptorFactory.newConstraintDescriptor(
            name = context.fieldName.orNull,
            clazz = scalaType.erasure,
            declaringClazz = context.rootClazz.getOrElse(scalaType.erasure),
            annotation = constraint
          )
      }

      if (!ignorable(value, constraint) && groupsEnabled(constraintDescriptor, groups)) {
        val refinedScalaType =
          Types.refineScalaType(value, scalaType)

        val constraintValidator: ConstraintValidator[Annotation, Any] = validatorOption match {
          case Some(validator) =>
            validator // should already be initialized
          case _ =>
            constraintValidatorManager
              .getInitializedValidator[Annotation](
                Types.getJavaType(refinedScalaType),
                constraintDescriptor,
                validatorFactory.constraintValidatorFactory,
                validatorFactory.validatorFactoryScopedContext.getConstraintValidatorInitializationContext
              ).asInstanceOf[ConstraintValidator[Annotation, Any]]
        }

        if (constraintValidator == null) {
          val configuration =
            if (context.path.toString.isEmpty) Classes.simpleName(scalaType.erasure)
            else context.path.toString
          throw new UnexpectedTypeException(
            s"No validator could be found for constraint '${constraint.annotationType()}' " +
              s"validating type '${scalaType.erasure.getName}'. " +
              s"Check configuration for '$configuration'")
        }
        // create validator context
        val constraintValidatorContext: ConstraintValidatorContext =
          constraintValidatorContextFactory.newConstraintValidatorContext(
            context.path,
            constraintDescriptor)
        // compute if valid
        if (constraintValidator.isValid(value, constraintValidatorContext)) Set.empty
        else {
          constraintViolationFactory.buildConstraintViolations[T](
            rootClazz = context.rootClazz,
            root = context.root,
            leaf = context.leaf,
            path = context.path,
            invalidValue = value,
            constraintDescriptor = constraintDescriptor,
            constraintValidatorContext = constraintValidatorContext
          )
        }
      } else Set.empty
  }

  private[this] def isValidOption[T](
    context: ValidationContext[T],
    constraint: Annotation,
    scalaType: ScalaType,
    value: Any,
    constraintDescriptorOption: Option[ConstraintDescriptorImpl[Annotation]],
    validatorOption: Option[ConstraintValidator[Annotation, Any]],
    groups: Seq[Class[_]]
  ): Set[ConstraintViolation[T]] = value match {
    case Some(actualVal) =>
      isValid[T](
        context,
        constraint,
        scalaType,
        actualVal,
        constraintDescriptorOption,
        validatorOption,
        groups)
    case _ =>
      Set.empty[ConstraintViolation[T]]
  }

  private[this] def groupsEnabled(
    constraintDescriptor: ConstraintDescriptor[_],
    groups: Seq[Class[_]]
  ): Boolean = {
    val groupsFromAnnotation: java.util.Set[Class[_]] = constraintDescriptor.getGroups
    if (groups.isEmpty && groupsFromAnnotation.isEmpty) true
    else if (groups.isEmpty &&
      groupsFromAnnotation.contains(classOf[jakarta.validation.groups.Default])) true
    else if (groups.contains(classOf[jakarta.validation.groups.Default]) &&
      groupsFromAnnotation.isEmpty) true
    else groups.exists(groupsFromAnnotation.contains)
  }

  /** @note this method is memoized as it should only ever need to be calculated once for a given [[Executable]] */
  private[this] val getExecutableParameterNames: Executable => Array[String] = Memoize {
    executable: Executable =>
      val parameterNameProviderNames: java.util.List[String] =
        validatorFactory.validatorFactoryScopedContext.getParameterNameProvider
          .getParameterNames(executable)
      // depending on the underlying list this may be linear and not constant lookup
      Array.tabulate(parameterNameProviderNames.size()) { index =>
        parameterNameProviderNames.get(index)
      }
  }

  /** @note this method is memoized as it should only ever need to be calculated once for a given [[ConstraintValidator]] */
  private[this] val parametersValidationTargetFilter: ConstraintValidator[_, _] => Boolean =
    Memoize { constraintValidator: ConstraintValidator[_, _] =>
      ReflectAnnotations
        .findAnnotation(
          classOf[SupportedValidationTarget],
          constraintValidator.getClass.getAnnotations) match {
        case Some(annotation: SupportedValidationTarget) =>
          annotation.value().contains(ValidationTarget.PARAMETERS)
        case _ => false
      }
    }

  /** @note this method is memoized as it should only ever need to be calculated once for a given [[Executable]] */
  private[this] val getExecutableMetaData: Executable => ExecutableMetaData = Memoize {
    executable: Executable =>
      val callable: ExecutableCallable[_] = executable match {
        case constructor: Constructor[_] =>
          new ConstructorCallable(constructor)
        case method: Method =>
          new MethodCallable(method)
      }
      val builder = new ExecutableMetaData.Builder(
        executable.getDeclaringClass,
        new ConstrainedExecutable(
          ConfigurationSource.ANNOTATION,
          callable,
          callable.getParameters
            .map(p =>
              new ConstrainedParameter(
                ConfigurationSource.ANNOTATION,
                callable,
                p.getType,
                p.getIndex)).asJava,
          Collections.emptySet(),
          Collections.emptySet(),
          Collections.emptySet(),
          CascadingMetaDataBuilder.nonCascading
        ),
        validatorFactory.getConstraintCreationContext,
        validatorFactory.getExecutableHelper,
        validatorFactory.getExecutableParameterNameProvider,
        validatorFactory.getMethodValidationConfiguration
      )

      builder.build()
  }

  private[validation] def parseMethodValidationFailures[T](
    rootClazz: Option[Class[T]],
    root: Option[T],
    leaf: Option[Any],
    path: PathImpl,
    annotation: MethodValidation,
    method: Method,
    result: MethodValidationResult,
    value: Any,
    constraintDescriptor: ConstraintDescriptor[_]
  ): Set[ConstraintViolation[T]] = {
    def methodValidationConstraintViolation(
      validationPath: PathImpl,
      message: String,
      payload: Payload
    ): ConstraintViolation[T] = {
      constraintViolationFactory.newConstraintViolation[T](
        messageTemplate = annotation.message(),
        interpolatedMessage = message,
        path = validationPath,
        invalidValue = value.asInstanceOf[T],
        rootClazz = rootClazz.orNull,
        root = root.getOrElse(null.asInstanceOf[T]),
        leaf = leaf.orNull,
        constraintDescriptor = constraintDescriptor,
        payload = payload
      )
    }

    result match {
      case invalid: MethodValidationResult.Invalid =>
        val results = mutable.HashSet[ConstraintViolation[T]]()
        val annotationFields: Array[String] = annotation.fields.filter(_.nonEmpty)
        if (annotationFields.nonEmpty) {
          var index = 0
          val length = annotationFields.length
          while (index < length) {
            val fieldName = annotationFields(index)
            val parameterPath = PathImpl.createCopy(path)
            parameterPath.addParameterNode(fieldName, index)
            results.add(
              methodValidationConstraintViolation(
                parameterPath,
                invalid.message,
                invalid.payload.orNull)
            )
            index += 1
          }
          if (results.nonEmpty) results.toSet
          else Set.empty[ConstraintViolation[T]]
        } else {
          Set(methodValidationConstraintViolation(path, invalid.message, invalid.payload.orNull))
        }
      case _ => Set.empty[ConstraintViolation[T]]
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy