/**
 * Check that the number being validated is less than or equal to the maximum value specified.
 *
 * @author Hardy Ferentschik
 */
public class DecimalMaxValidatorForNumber implements ConstraintValidator<DecimalMax, Number> {

  private static final Log log = LoggerFactory.make();

  private BigDecimal maxValue;

  public void initialize(DecimalMax maxValue) {
    try {
      this.maxValue = new BigDecimal(maxValue.value());
    } catch (NumberFormatException nfe) {
      throw log.getInvalidBigDecimalFormatException(maxValue.value(), nfe);
    }
  }

  public boolean isValid(Number value, ConstraintValidatorContext constraintValidatorContext) {
    // null values are valid
    if (value == null) {
      return true;
    }

    if (value instanceof BigDecimal) {
      return ((BigDecimal) value).compareTo(maxValue) != 1;
    } else if (value instanceof BigInteger) {
      return (new BigDecimal((BigInteger) value)).compareTo(maxValue) != 1;
    } else if (value instanceof Long) {
      return (BigDecimal.valueOf(value.longValue()).compareTo(maxValue)) != 1;
    } else {
      return (BigDecimal.valueOf(value.doubleValue()).compareTo(maxValue)) != 1;
    }
  }
}
/**
 * @author Emmanuel Bernard
 * @author Hardy Ferentschik
 */
public final class GetAnnotationParameter<T> implements PrivilegedAction<T> {

  private static final Log log = LoggerFactory.make();

  private final Annotation annotation;
  private final String parameterName;
  private final Class<T> type;

  public static <T> GetAnnotationParameter<T> action(
      Annotation annotation, String parameterName, Class<T> type) {
    return new GetAnnotationParameter<T>(annotation, parameterName, type);
  }

  private GetAnnotationParameter(Annotation annotation, String parameterName, Class<T> type) {
    this.annotation = annotation;
    this.parameterName = parameterName;
    this.type = type;
  }

  @Override
  public T run() {
    try {
      Method m = annotation.getClass().getMethod(parameterName);
      m.setAccessible(true);
      Object o = m.invoke(annotation);
      if (type.isAssignableFrom(o.getClass())) {
        return (T) o;
      } else {
        throw log.getWrongParameterTypeException(type.getName(), o.getClass().getName());
      }
    } catch (NoSuchMethodException e) {
      throw log.getUnableToFindAnnotationParameterException(parameterName, e);
    } catch (IllegalAccessException e) {
      throw log.getUnableToGetAnnotationParameterException(
          parameterName, annotation.getClass().getName(), e);
    } catch (InvocationTargetException e) {
      throw log.getUnableToGetAnnotationParameterException(
          parameterName, annotation.getClass().getName(), e);
    }
  }
}
/**
 * ModCheckBase contains all shared methods and options used by Mod Check Validators
 *
 * <p>http://en.wikipedia.org/wiki/Check_digit
 *
 * @author George Gastaldi
 * @author Hardy Ferentschik
 * @author Victor Rezende dos Santos
 */
public abstract class ModCheckBase {

  private static final Log log = LoggerFactory.make();

  private static final Pattern NUMBERS_ONLY_REGEXP = Pattern.compile("[^0-9]");

  private static final int DEC_RADIX = 10;

  /** The start index for the checksum calculation */
  private int startIndex;

  /** The end index for the checksum calculation */
  private int endIndex;

  /** The index of the checksum digit */
  private int checkDigitIndex;

  private boolean ignoreNonDigitCharacters;

  public boolean isValid(final CharSequence value, final ConstraintValidatorContext context) {
    if (value == null) {
      return true;
    }

    String valueAsString = value.toString();
    String digitsAsString;
    char checkDigit;
    try {
      digitsAsString = extractVerificationString(valueAsString);
      checkDigit = extractCheckDigit(valueAsString);
    } catch (IndexOutOfBoundsException e) {
      return false;
    }
    digitsAsString = stripNonDigitsIfRequired(digitsAsString);

    List<Integer> digits;
    try {
      digits = extractDigits(digitsAsString);
    } catch (NumberFormatException e) {
      return false;
    }

    return this.isCheckDigitValid(digits, checkDigit);
  }

  public abstract boolean isCheckDigitValid(List<Integer> digits, char checkDigit);

  protected void initialize(
      int startIndex, int endIndex, int checkDigitIndex, boolean ignoreNonDigitCharacters) {
    this.startIndex = startIndex;
    this.endIndex = endIndex;
    this.checkDigitIndex = checkDigitIndex;
    this.ignoreNonDigitCharacters = ignoreNonDigitCharacters;

    this.validateOptions();
  }

  /**
   * Returns the numeric {@code int} value of a {@code char}
   *
   * @param value the input {@code char} to be parsed
   * @return the numeric {@code int} value represented by the character.
   * @throws NumberFormatException in case character is not a digit
   */
  protected int extractDigit(char value) throws NumberFormatException {
    if (Character.isDigit(value)) {
      return Character.digit(value, DEC_RADIX);
    } else {
      throw log.getCharacterIsNotADigitException(value);
    }
  }

  /**
   * Parses the {@link String} value as a {@link List} of {@link Integer} objects
   *
   * @param value the input string to be parsed
   * @return List of {@code Integer} objects.
   * @throws NumberFormatException in case any of the characters is not a digit
   */
  private List<Integer> extractDigits(final String value) throws NumberFormatException {
    List<Integer> digits = new ArrayList<Integer>(value.length());
    char[] chars = value.toCharArray();
    for (char c : chars) {
      digits.add(extractDigit(c));
    }
    return digits;
  }

  private boolean validateOptions() {
    if (this.startIndex < 0) {
      throw log.getStartIndexCannotBeNegativeException(this.startIndex);
    }

    if (this.endIndex < 0) {
      throw log.getEndIndexCannotBeNegativeException(this.endIndex);
    }

    if (this.startIndex > this.endIndex) {
      throw log.getInvalidRangeException(this.startIndex, this.endIndex);
    }

    if (this.checkDigitIndex > 0
        && this.startIndex <= this.checkDigitIndex
        && this.endIndex > this.checkDigitIndex) {
      throw log.getInvalidCheckDigitException(this.startIndex, this.endIndex);
    }

    return true;
  }

  private String stripNonDigitsIfRequired(String value) {
    if (ignoreNonDigitCharacters) {
      return NUMBERS_ONLY_REGEXP.matcher(value).replaceAll("");
    } else {
      return value;
    }
  }

  private String extractVerificationString(String value) throws IndexOutOfBoundsException {
    // the string contains the check digit, just return the digits to verify
    if (endIndex == Integer.MAX_VALUE) {
      return value.substring(0, value.length() - 1);
    } else if (checkDigitIndex == -1) {
      return value.substring(startIndex, endIndex);
    } else {
      return value.substring(startIndex, endIndex + 1);
    }
  }

  private char extractCheckDigit(String value) throws IndexOutOfBoundsException {
    // take last character of string to be validated unless the index is given explicitly
    if (checkDigitIndex == -1) {
      if (endIndex == Integer.MAX_VALUE) {
        return value.charAt(value.length() - 1);
      } else {
        return value.charAt(endIndex);
      }
    } else {
      return value.charAt(checkDigitIndex);
    }
  }
}
/**
 * A CDI portable extension which integrates Bean Validation with CDI. It registers the following
 * objects:
 *
 * <ul>
 *   <li>Beans for {@link ValidatorFactory} and {@link Validator} representing default validator
 *       factory and validator as configured via {@code META-INF/validation.xml}. These beans will
 *       have the {@code Default} qualifier and in addition the {@code HibernateValidator} qualifier
 *       if Hibernate Validator is the default validation provider.
 *   <li>In case Hibernate Validator is <em>not</em> the default provider, another pair of beans
 *       will be registered in addition which are qualified with the {@code HibernateValidator}
 *       qualifier.
 * </ul>
 *
 * Neither of these beans will be registered in case there is already another bean with the same
 * type and qualifier(s), e.g. registered by another portable extension or the application itself.
 *
 * @author Gunnar Morling
 * @author Hardy Ferentschik
 */
public class ValidationExtension implements Extension {

  private static final Log log = LoggerFactory.make();

  private static final EnumSet<ExecutableType> ALL_EXECUTABLE_TYPES =
      EnumSet.of(
          ExecutableType.CONSTRUCTORS,
          ExecutableType.NON_GETTER_METHODS,
          ExecutableType.GETTER_METHODS);
  private static final EnumSet<ExecutableType> DEFAULT_EXECUTABLE_TYPES =
      EnumSet.of(ExecutableType.CONSTRUCTORS, ExecutableType.NON_GETTER_METHODS);

  @SuppressWarnings("serial")
  private final Annotation defaultQualifier = new AnnotationLiteral<Default>() {};

  @SuppressWarnings("serial")
  private final Annotation hibernateValidatorQualifier =
      new AnnotationLiteral<HibernateValidator>() {};

  private final ExecutableHelper executableHelper;

  /** Used for identifying constrained classes */
  private final Validator validator;

  private final ValidatorFactory validatorFactory;
  private final Set<ExecutableType> globalExecutableTypes;
  private final boolean isExecutableValidationEnabled;

  private Bean<?> defaultValidatorFactoryBean;
  private Bean<?> hibernateValidatorFactoryBean;

  private Bean<?> defaultValidatorBean;
  private Bean<?> hibernateValidatorBean;

  public ValidationExtension() {
    Configuration<?> config = Validation.byDefaultProvider().configure();
    BootstrapConfiguration bootstrap = config.getBootstrapConfiguration();
    globalExecutableTypes = bootstrap.getDefaultValidatedExecutableTypes();
    isExecutableValidationEnabled = bootstrap.isExecutableValidationEnabled();
    validatorFactory = config.buildValidatorFactory();
    validator = validatorFactory.getValidator();

    executableHelper = new ExecutableHelper(new TypeResolutionHelper());
  }

  /**
   * Used to register the method validation interceptor binding annotation.
   *
   * @param beforeBeanDiscoveryEvent event fired before the bean discovery process starts
   * @param beanManager the bean manager.
   */
  public void beforeBeanDiscovery(
      @Observes BeforeBeanDiscovery beforeBeanDiscoveryEvent, final BeanManager beanManager) {
    Contracts.assertNotNull(
        beforeBeanDiscoveryEvent, "The BeforeBeanDiscovery event cannot be null");
    Contracts.assertNotNull(beanManager, "The BeanManager cannot be null");

    // Register the interceptor explicitly. This way, no beans.xml is needed
    AnnotatedType<ValidationInterceptor> annotatedType =
        beanManager.createAnnotatedType(ValidationInterceptor.class);
    beforeBeanDiscoveryEvent.addAnnotatedType(annotatedType);
  }

  /**
   * Registers beans for {@code ValidatorFactory} and {@code Validator} if not yet present.
   *
   * @param afterBeanDiscoveryEvent event fired after the bean discovery phase.
   * @param beanManager the bean manager.
   */
  public void afterBeanDiscovery(
      @Observes AfterBeanDiscovery afterBeanDiscoveryEvent, BeanManager beanManager) {
    Contracts.assertNotNull(afterBeanDiscoveryEvent, "The AfterBeanDiscovery event cannot be null");
    Contracts.assertNotNull(beanManager, "The BeanManager cannot be null");

    ValidationProviderHelper defaultProviderHelper =
        ValidationProviderHelper.forDefaultProvider(validatorFactory);
    ValidationProviderHelper hvProviderHelper = ValidationProviderHelper.forHibernateValidator();

    // register default VF if none has been provided by the application or another PE
    if (defaultValidatorFactoryBean == null) {
      defaultValidatorFactoryBean = new ValidatorFactoryBean(beanManager, defaultProviderHelper);
      if (hibernateValidatorFactoryBean == null && defaultProviderHelper.isHibernateValidator()) {
        hibernateValidatorFactoryBean = defaultValidatorFactoryBean;
      }
      afterBeanDiscoveryEvent.addBean(defaultValidatorFactoryBean);
    }

    // register VF with @HibernateValidator qualifier in case it hasn't been contributed by the
    // application and the
    // default VF registered by ourselves isn't for Hibernate Validator
    if (hibernateValidatorFactoryBean == null) {
      hibernateValidatorFactoryBean = new ValidatorFactoryBean(beanManager, hvProviderHelper);
      afterBeanDiscoveryEvent.addBean(hibernateValidatorFactoryBean);
    }

    // register default validator if required
    if (defaultValidatorBean == null) {
      defaultValidatorBean =
          new ValidatorBean(beanManager, defaultValidatorFactoryBean, defaultProviderHelper);
      if (hibernateValidatorBean == null && defaultProviderHelper.isHibernateValidator()) {
        hibernateValidatorBean = defaultValidatorBean;
      }
      afterBeanDiscoveryEvent.addBean(defaultValidatorBean);
    }

    // register validator with @HibernateValidator if required
    if (hibernateValidatorBean == null) {
      hibernateValidatorBean =
          new ValidatorBean(beanManager, hibernateValidatorFactoryBean, hvProviderHelper);
      afterBeanDiscoveryEvent.addBean(hibernateValidatorBean);
    }
  }

  /**
   * Watches the {@code ProcessBean} event in order to determine whether beans for {@code
   * ValidatorFactory} and {@code Validator} already have been registered by some other component.
   *
   * @param processBeanEvent event fired for each enabled bean.
   */
  public void processBean(@Observes ProcessBean<?> processBeanEvent) {
    Contracts.assertNotNull(processBeanEvent, "The ProcessBean event cannot be null");

    Bean<?> bean = processBeanEvent.getBean();

    if (bean.getTypes().contains(ValidatorFactory.class) || bean instanceof ValidatorFactoryBean) {
      if (bean.getQualifiers().contains(defaultQualifier)) {
        defaultValidatorFactoryBean = bean;
      }
      if (bean.getQualifiers().contains(hibernateValidatorQualifier)) {
        hibernateValidatorFactoryBean = bean;
      }
    } else if (bean.getTypes().contains(Validator.class) || bean instanceof ValidatorBean) {
      if (bean.getQualifiers().contains(defaultQualifier)) {
        defaultValidatorBean = bean;
      }
      if (bean.getQualifiers().contains(hibernateValidatorQualifier)) {
        hibernateValidatorBean = bean;
      }
    }
  }

  /**
   * Used to register the method validation interceptor bindings.
   *
   * @param processAnnotatedTypeEvent event fired for each annotated type
   * @param <T> the annotated type
   */
  public <T> void processAnnotatedType(
      @Observes @WithAnnotations({Constraint.class, Valid.class, ValidateOnExecution.class})
          ProcessAnnotatedType<T> processAnnotatedTypeEvent) {
    Contracts.assertNotNull(
        processAnnotatedTypeEvent, "The ProcessAnnotatedType event cannot be null");

    // validation globally disabled
    if (!isExecutableValidationEnabled) {
      return;
    }

    AnnotatedType<T> type = processAnnotatedTypeEvent.getAnnotatedType();
    Set<AnnotatedCallable<? super T>> constrainedCallables = determineConstrainedCallables(type);

    if (!constrainedCallables.isEmpty()) {
      ValidationEnabledAnnotatedType<T> wrappedType =
          new ValidationEnabledAnnotatedType<T>(type, constrainedCallables);
      processAnnotatedTypeEvent.setAnnotatedType(wrappedType);
    }
  }

  private <T> Set<AnnotatedCallable<? super T>> determineConstrainedCallables(
      AnnotatedType<T> type) {
    Set<AnnotatedCallable<? super T>> callables = newHashSet();
    BeanDescriptor beanDescriptor = validator.getConstraintsForClass(type.getJavaClass());

    determineConstrainedConstructors(type, beanDescriptor, callables);
    determineConstrainedMethods(type, beanDescriptor, callables);

    return callables;
  }

  private <T> void determineConstrainedMethods(
      AnnotatedType<T> type,
      BeanDescriptor beanDescriptor,
      Set<AnnotatedCallable<? super T>> callables) {
    List<Method> overriddenAndImplementedMethods =
        InheritedMethodsHelper.getAllMethods(type.getJavaClass());

    for (AnnotatedMethod<? super T> annotatedMethod : type.getMethods()) {
      Method method = annotatedMethod.getJavaMember();

      boolean isGetter = ReflectionHelper.isGetterMethod(method);

      // obtain @ValidateOnExecution from the top-most method in the hierarchy
      Method methodForExecutableTypeRetrieval =
          replaceWithOverriddenOrInterfaceMethod(method, overriddenAndImplementedMethods);

      EnumSet<ExecutableType> classLevelExecutableTypes =
          executableTypesDefinedOnType(methodForExecutableTypeRetrieval.getDeclaringClass());
      EnumSet<ExecutableType> memberLevelExecutableType =
          executableTypesDefinedOnMethod(methodForExecutableTypeRetrieval, isGetter);

      ExecutableType currentExecutableType =
          isGetter ? ExecutableType.GETTER_METHODS : ExecutableType.NON_GETTER_METHODS;

      // validation is enabled per default, so explicit configuration can just veto whether
      // validation occurs
      if (veto(classLevelExecutableTypes, memberLevelExecutableType, currentExecutableType)) {
        continue;
      }

      boolean needsValidation;
      if (isGetter) {
        needsValidation = isGetterConstrained(method, beanDescriptor);
      } else {
        needsValidation = isNonGetterConstrained(method, beanDescriptor);
      }

      if (needsValidation) {
        callables.add(annotatedMethod);
      }
    }
  }

  private <T> void determineConstrainedConstructors(
      AnnotatedType<T> type,
      BeanDescriptor beanDescriptor,
      Set<AnnotatedCallable<? super T>> callables) {
    Class<?> clazz = type.getJavaClass();
    EnumSet<ExecutableType> classLevelExecutableTypes = executableTypesDefinedOnType(clazz);

    for (AnnotatedConstructor<T> annotatedConstructor : type.getConstructors()) {
      Constructor<?> constructor = annotatedConstructor.getJavaMember();
      EnumSet<ExecutableType> memberLevelExecutableType =
          executableTypesDefinedOnConstructor(constructor);

      if (veto(classLevelExecutableTypes, memberLevelExecutableType, ExecutableType.CONSTRUCTORS)) {
        continue;
      }

      if (beanDescriptor.getConstraintsForConstructor(constructor.getParameterTypes()) != null) {
        callables.add(annotatedConstructor);
      }
    }
  }

  private boolean isNonGetterConstrained(Method method, BeanDescriptor beanDescriptor) {
    return beanDescriptor.getConstraintsForMethod(method.getName(), method.getParameterTypes())
        != null;
  }

  private boolean isGetterConstrained(Method method, BeanDescriptor beanDescriptor) {
    String propertyName = ReflectionHelper.getPropertyName(method);
    PropertyDescriptor propertyDescriptor = beanDescriptor.getConstraintsForProperty(propertyName);
    return propertyDescriptor != null
        && propertyDescriptor.findConstraints().declaredOn(ElementType.METHOD).hasConstraints();
  }

  private boolean veto(
      EnumSet<ExecutableType> classLevelExecutableTypes,
      EnumSet<ExecutableType> memberLevelExecutableType,
      ExecutableType currentExecutableType) {
    if (!memberLevelExecutableType.isEmpty()) {
      return !memberLevelExecutableType.contains(currentExecutableType)
          && !memberLevelExecutableType.contains(ExecutableType.IMPLICIT);
    }

    if (!classLevelExecutableTypes.isEmpty()) {
      return !classLevelExecutableTypes.contains(currentExecutableType)
          && !classLevelExecutableTypes.contains(ExecutableType.IMPLICIT);
    }

    return !globalExecutableTypes.contains(currentExecutableType);
  }

  private EnumSet<ExecutableType> executableTypesDefinedOnType(Class<?> clazz) {
    ValidateOnExecution validateOnExecutionAnnotation =
        clazz.getAnnotation(ValidateOnExecution.class);
    EnumSet<ExecutableType> executableTypes =
        commonExecutableTypeChecks(validateOnExecutionAnnotation);

    if (executableTypes.contains(ExecutableType.IMPLICIT)) {
      return DEFAULT_EXECUTABLE_TYPES;
    }

    return executableTypes;
  }

  private EnumSet<ExecutableType> executableTypesDefinedOnMethod(Method method, boolean isGetter) {
    ValidateOnExecution validateOnExecutionAnnotation =
        method.getAnnotation(ValidateOnExecution.class);
    EnumSet<ExecutableType> executableTypes =
        commonExecutableTypeChecks(validateOnExecutionAnnotation);

    if (executableTypes.contains(ExecutableType.IMPLICIT)) {
      if (isGetter) {
        executableTypes.add(ExecutableType.GETTER_METHODS);
      } else {
        executableTypes.add(ExecutableType.NON_GETTER_METHODS);
      }
    }

    return executableTypes;
  }

  private EnumSet<ExecutableType> executableTypesDefinedOnConstructor(Constructor<?> constructor) {
    ValidateOnExecution validateOnExecutionAnnotation =
        constructor.getAnnotation(ValidateOnExecution.class);
    EnumSet<ExecutableType> executableTypes =
        commonExecutableTypeChecks(validateOnExecutionAnnotation);

    if (executableTypes.contains(ExecutableType.IMPLICIT)) {
      executableTypes.add(ExecutableType.CONSTRUCTORS);
    }

    return executableTypes;
  }

  private EnumSet<ExecutableType> commonExecutableTypeChecks(
      ValidateOnExecution validateOnExecutionAnnotation) {
    if (validateOnExecutionAnnotation == null) {
      return EnumSet.noneOf(ExecutableType.class);
    }

    EnumSet<ExecutableType> executableTypes = EnumSet.noneOf(ExecutableType.class);
    if (validateOnExecutionAnnotation.type().length == 0) { // HV-757
      executableTypes.add(ExecutableType.NONE);
    } else {
      Collections.addAll(executableTypes, validateOnExecutionAnnotation.type());
    }

    // IMPLICIT cannot be mixed 10.1.2 of spec - Mixing IMPLICIT and other executable types is
    // illegal
    if (executableTypes.contains(ExecutableType.IMPLICIT) && executableTypes.size() > 1) {
      throw log.getMixingImplicitWithOtherExecutableTypesException();
    }

    // NONE can be removed 10.1.2 of spec - A list containing NONE and other types of executables is
    // equivalent to a
    // list containing the types of executables without NONE.
    if (executableTypes.contains(ExecutableType.NONE) && executableTypes.size() > 1) {
      executableTypes.remove(ExecutableType.NONE);
    }

    // 10.1.2 of spec - A list containing ALL and other types of executables is equivalent to a list
    // containing only ALL
    if (executableTypes.contains(ExecutableType.ALL)) {
      executableTypes = ALL_EXECUTABLE_TYPES;
    }

    return executableTypes;
  }

  public Method replaceWithOverriddenOrInterfaceMethod(
      Method method, List<Method> allMethodsOfType) {
    LinkedList<Method> list = new LinkedList<Method>(allMethodsOfType);
    Iterator<Method> iterator = list.descendingIterator();
    while (iterator.hasNext()) {
      Method overriddenOrInterfaceMethod = iterator.next();
      if (executableHelper.overrides(method, overriddenOrInterfaceMethod)) {
        if (method.getAnnotation(ValidateOnExecution.class) != null) {
          throw log.getValidateOnExecutionOnOverriddenOrInterfaceMethodException(method);
        }
        return overriddenOrInterfaceMethod;
      }
    }

    return method;
  }
}
/**
 * Resource bundle backed message interpolator.
 *
 * @author Emmanuel Bernard
 * @author Hardy Ferentschik
 * @author Gunnar Morling
 * @author Adam Stawicki
 */
public abstract class AbstractMessageInterpolator implements MessageInterpolator {
  private static final Log log = LoggerFactory.make();

  /** The default initial capacity for this cache. */
  private static final int DEFAULT_INITIAL_CAPACITY = 100;

  /** The default load factor for this cache. */
  private static final float DEFAULT_LOAD_FACTOR = 0.75f;

  /** The default concurrency level for this cache. */
  private static final int DEFAULT_CONCURRENCY_LEVEL = 16;

  /** The name of the default message bundle. */
  private static final String DEFAULT_VALIDATION_MESSAGES =
      "org.hibernate.validator.ValidationMessages";

  /** The name of the user-provided message bundle as defined in the specification. */
  public static final String USER_VALIDATION_MESSAGES = "ValidationMessages";

  /** The default locale in the current JVM. */
  private final Locale defaultLocale;

  /** Loads user-specified resource bundles. */
  private final ResourceBundleLocator userResourceBundleLocator;

  /** Loads built-in resource bundles. */
  private final ResourceBundleLocator defaultResourceBundleLocator;

  /** Step 1-3 of message interpolation can be cached. We do this in this map. */
  private final ConcurrentReferenceHashMap<LocalizedMessage, String> resolvedMessages;

  /**
   * Step 4 of message interpolation replaces message parameters. The token list for message
   * parameters is cached in this map.
   */
  private final ConcurrentReferenceHashMap<String, List<Token>> tokenizedParameterMessages;

  /**
   * Step 5 of message interpolation replaces EL expressions. The token list for EL expressions is
   * cached in this map.
   */
  private final ConcurrentReferenceHashMap<String, List<Token>> tokenizedELMessages;

  /** Flag indicating whether this interpolator should cache some of the interpolation steps. */
  private final boolean cachingEnabled;

  private static final Pattern LEFT_BRACE = Pattern.compile("\\{", Pattern.LITERAL);
  private static final Pattern RIGHT_BRACE = Pattern.compile("\\}", Pattern.LITERAL);
  private static final Pattern SLASH = Pattern.compile("\\\\", Pattern.LITERAL);
  private static final Pattern DOLLAR = Pattern.compile("\\$", Pattern.LITERAL);

  public AbstractMessageInterpolator() {
    this(null);
  }

  public AbstractMessageInterpolator(ResourceBundleLocator userResourceBundleLocator) {
    this(userResourceBundleLocator, true);
  }

  public AbstractMessageInterpolator(
      ResourceBundleLocator userResourceBundleLocator, boolean cacheMessages) {
    defaultLocale = Locale.getDefault();

    if (userResourceBundleLocator == null) {
      this.userResourceBundleLocator = new PlatformResourceBundleLocator(USER_VALIDATION_MESSAGES);
    } else {
      this.userResourceBundleLocator = userResourceBundleLocator;
    }

    this.defaultResourceBundleLocator =
        new PlatformResourceBundleLocator(DEFAULT_VALIDATION_MESSAGES);
    this.cachingEnabled = cacheMessages;

    if (cachingEnabled) {
      this.resolvedMessages =
          new ConcurrentReferenceHashMap<LocalizedMessage, String>(
              DEFAULT_INITIAL_CAPACITY,
              DEFAULT_LOAD_FACTOR,
              DEFAULT_CONCURRENCY_LEVEL,
              SOFT,
              SOFT,
              EnumSet.noneOf(ConcurrentReferenceHashMap.Option.class));
      this.tokenizedParameterMessages =
          new ConcurrentReferenceHashMap<String, List<Token>>(
              DEFAULT_INITIAL_CAPACITY,
              DEFAULT_LOAD_FACTOR,
              DEFAULT_CONCURRENCY_LEVEL,
              SOFT,
              SOFT,
              EnumSet.noneOf(ConcurrentReferenceHashMap.Option.class));
      this.tokenizedELMessages =
          new ConcurrentReferenceHashMap<String, List<Token>>(
              DEFAULT_INITIAL_CAPACITY,
              DEFAULT_LOAD_FACTOR,
              DEFAULT_CONCURRENCY_LEVEL,
              SOFT,
              SOFT,
              EnumSet.noneOf(ConcurrentReferenceHashMap.Option.class));
    } else {
      resolvedMessages = null;
      tokenizedParameterMessages = null;
      tokenizedELMessages = null;
    }
  }

  @Override
  public String interpolate(String message, Context context) {
    // probably no need for caching, but it could be done by parameters since the map
    // is immutable and uniquely built per Validation definition, the comparison has to be based on
    // == and not equals though
    String interpolatedMessage = message;
    try {
      interpolatedMessage = interpolateMessage(message, context, defaultLocale);
    } catch (MessageDescriptorFormatException e) {
      log.warn(e.getMessage());
    }
    return interpolatedMessage;
  }

  @Override
  public String interpolate(String message, Context context, Locale locale) {
    String interpolatedMessage = message;
    try {
      interpolatedMessage = interpolateMessage(message, context, locale);
    } catch (ValidationException e) {
      log.warn(e.getMessage());
    }
    return interpolatedMessage;
  }

  /**
   * Runs the message interpolation according to algorithm specified in the Bean Validation
   * specification. <br>
   * Note: <br>
   * Look-ups in user bundles is recursive whereas look-ups in default bundle are not!
   *
   * @param message the message to interpolate
   * @param context the context for this interpolation
   * @param locale the {@code Locale} to use for the resource bundle.
   * @return the interpolated message.
   */
  private String interpolateMessage(String message, Context context, Locale locale)
      throws MessageDescriptorFormatException {
    LocalizedMessage localisedMessage = new LocalizedMessage(message, locale);
    String resolvedMessage = null;

    if (cachingEnabled) {
      resolvedMessage = resolvedMessages.get(localisedMessage);
    }

    // if the message is not already in the cache we have to run step 1-3 of the message resolution
    if (resolvedMessage == null) {
      ResourceBundle userResourceBundle = userResourceBundleLocator.getResourceBundle(locale);
      ResourceBundle defaultResourceBundle = defaultResourceBundleLocator.getResourceBundle(locale);

      String userBundleResolvedMessage;
      resolvedMessage = message;
      boolean evaluatedDefaultBundleOnce = false;
      do {
        // search the user bundle recursive (step1)
        userBundleResolvedMessage =
            interpolateBundleMessage(resolvedMessage, userResourceBundle, locale, true);

        // exit condition - we have at least tried to validate against the default bundle and there
        // was no
        // further replacements
        if (evaluatedDefaultBundleOnce
            && !hasReplacementTakenPlace(userBundleResolvedMessage, resolvedMessage)) {
          break;
        }

        // search the default bundle non recursive (step2)
        resolvedMessage =
            interpolateBundleMessage(
                userBundleResolvedMessage, defaultResourceBundle, locale, false);
        evaluatedDefaultBundleOnce = true;
      } while (true);
    }

    // cache resolved message
    if (cachingEnabled) {
      String cachedResolvedMessage =
          resolvedMessages.putIfAbsent(localisedMessage, resolvedMessage);
      if (cachedResolvedMessage != null) {
        resolvedMessage = cachedResolvedMessage;
      }
    }

    // resolve parameter expressions (step 4)
    List<Token> tokens = null;
    if (cachingEnabled) {
      tokens = tokenizedParameterMessages.get(resolvedMessage);
    }
    if (tokens == null) {
      TokenCollector tokenCollector =
          new TokenCollector(resolvedMessage, InterpolationTermType.PARAMETER);
      tokens = tokenCollector.getTokenList();

      if (cachingEnabled) {
        tokenizedParameterMessages.putIfAbsent(resolvedMessage, tokens);
      }
    }
    resolvedMessage = interpolateExpression(new TokenIterator(tokens), context, locale);

    // resolve EL expressions (step 5)
    tokens = null;
    if (cachingEnabled) {
      tokens = tokenizedELMessages.get(resolvedMessage);
    }
    if (tokens == null) {
      TokenCollector tokenCollector = new TokenCollector(resolvedMessage, InterpolationTermType.EL);
      tokens = tokenCollector.getTokenList();

      if (cachingEnabled) {
        tokenizedELMessages.putIfAbsent(resolvedMessage, tokens);
      }
    }
    resolvedMessage = interpolateExpression(new TokenIterator(tokens), context, locale);

    // last but not least we have to take care of escaped literals
    resolvedMessage = replaceEscapedLiterals(resolvedMessage);

    return resolvedMessage;
  }

  private String replaceEscapedLiterals(String resolvedMessage) {
    resolvedMessage = LEFT_BRACE.matcher(resolvedMessage).replaceAll("{");
    resolvedMessage = RIGHT_BRACE.matcher(resolvedMessage).replaceAll("}");
    resolvedMessage = SLASH.matcher(resolvedMessage).replaceAll(Matcher.quoteReplacement("\\"));
    resolvedMessage = DOLLAR.matcher(resolvedMessage).replaceAll(Matcher.quoteReplacement("$"));
    return resolvedMessage;
  }

  private boolean hasReplacementTakenPlace(String origMessage, String newMessage) {
    return !origMessage.equals(newMessage);
  }

  private String interpolateBundleMessage(
      String message, ResourceBundle bundle, Locale locale, boolean recursive)
      throws MessageDescriptorFormatException {
    TokenCollector tokenCollector = new TokenCollector(message, InterpolationTermType.PARAMETER);
    TokenIterator tokenIterator = new TokenIterator(tokenCollector.getTokenList());
    while (tokenIterator.hasMoreInterpolationTerms()) {
      String term = tokenIterator.nextInterpolationTerm();
      String resolvedParameterValue = resolveParameter(term, bundle, locale, recursive);
      tokenIterator.replaceCurrentInterpolationTerm(resolvedParameterValue);
    }
    return tokenIterator.getInterpolatedMessage();
  }

  private String interpolateExpression(TokenIterator tokenIterator, Context context, Locale locale)
      throws MessageDescriptorFormatException {
    while (tokenIterator.hasMoreInterpolationTerms()) {
      String term = tokenIterator.nextInterpolationTerm();

      String resolvedExpression = interpolate(context, locale, term);
      tokenIterator.replaceCurrentInterpolationTerm(resolvedExpression);
    }
    return tokenIterator.getInterpolatedMessage();
  }

  public abstract String interpolate(Context context, Locale locale, String term);

  private String resolveParameter(
      String parameterName, ResourceBundle bundle, Locale locale, boolean recursive)
      throws MessageDescriptorFormatException {
    String parameterValue;
    try {
      if (bundle != null) {
        parameterValue = bundle.getString(removeCurlyBraces(parameterName));
        if (recursive) {
          parameterValue = interpolateBundleMessage(parameterValue, bundle, locale, recursive);
        }
      } else {
        parameterValue = parameterName;
      }
    } catch (MissingResourceException e) {
      // return parameter itself
      parameterValue = parameterName;
    }
    return parameterValue;
  }

  private String removeCurlyBraces(String parameter) {
    return parameter.substring(1, parameter.length() - 1);
  }
}
/**
 * A constraint mapping creational context which allows to select the parameter or return value to
 * which the next operations shall apply.
 *
 * @author Kevin Pollet &lt;[email protected]&gt; (C) 2011 SERLI
 * @author Gunnar Morling
 */
class ExecutableConstraintMappingContextImpl
    implements ConstructorConstraintMappingContext, MethodConstraintMappingContext {

  private static final Log log = LoggerFactory.make();

  private final TypeConstraintMappingContextImpl<?> typeContext;
  private final ExecutableElement executable;
  private final ParameterConstraintMappingContextImpl[] parameterContexts;
  private ReturnValueConstraintMappingContextImpl returnValueContext;
  private CrossParameterConstraintMappingContextImpl crossParameterContext;

  ExecutableConstraintMappingContextImpl(
      TypeConstraintMappingContextImpl<?> typeContext, Constructor<?> constructor) {
    this(typeContext, ExecutableElement.forConstructor(constructor));
  }

  ExecutableConstraintMappingContextImpl(
      TypeConstraintMappingContextImpl<?> typeContext, Method method) {
    this(typeContext, ExecutableElement.forMethod(method));
  }

  private ExecutableConstraintMappingContextImpl(
      TypeConstraintMappingContextImpl<?> typeContext, ExecutableElement executable) {
    this.typeContext = typeContext;
    this.executable = executable;
    this.parameterContexts =
        new ParameterConstraintMappingContextImpl[executable.getParameterTypes().length];
  }

  @Override
  public ParameterConstraintMappingContext parameter(int index) {
    if (index < 0 || index >= executable.getParameterTypes().length) {
      throw log.getInvalidExecutableParameterIndexException(executable.getAsString(), index);
    }

    ParameterConstraintMappingContextImpl context = parameterContexts[index];

    if (context != null) {
      throw log.getParameterHasAlreadyBeConfiguredViaProgrammaticApiException(
          typeContext.getBeanClass().getName(), executable.getAsString(), index);
    }

    context = new ParameterConstraintMappingContextImpl(this, index);
    parameterContexts[index] = context;
    return context;
  }

  @Override
  public CrossParameterConstraintMappingContext crossParameter() {
    if (crossParameterContext != null) {
      throw log.getCrossParameterElementHasAlreadyBeConfiguredViaProgrammaticApiException(
          typeContext.getBeanClass().getName(), executable.getAsString());
    }

    crossParameterContext = new CrossParameterConstraintMappingContextImpl(this);
    return crossParameterContext;
  }

  @Override
  public ReturnValueConstraintMappingContext returnValue() {
    if (returnValueContext != null) {
      throw log.getReturnValueHasAlreadyBeConfiguredViaProgrammaticApiException(
          typeContext.getBeanClass().getName(), executable.getAsString());
    }

    returnValueContext = new ReturnValueConstraintMappingContextImpl(this);
    return returnValueContext;
  }

  public ExecutableElement getExecutable() {
    return executable;
  }

  public TypeConstraintMappingContextImpl<?> getTypeContext() {
    return typeContext;
  }

  public ConstrainedElement build(
      ConstraintHelper constraintHelper, ParameterNameProvider parameterNameProvider) {
    // TODO HV-919 Support specification of type parameter constraints via XML and API
    return new ConstrainedExecutable(
        ConfigurationSource.API,
        ConstraintLocation.forReturnValue(executable),
        getParameters(constraintHelper, parameterNameProvider),
        crossParameterContext != null
            ? crossParameterContext.getConstraints(constraintHelper)
            : Collections.<MetaConstraint<?>>emptySet(),
        returnValueContext != null
            ? returnValueContext.getConstraints(constraintHelper)
            : Collections.<MetaConstraint<?>>emptySet(),
        Collections.<MetaConstraint<?>>emptySet(),
        returnValueContext != null
            ? returnValueContext.getGroupConversions()
            : Collections.<Class<?>, Class<?>>emptyMap(),
        returnValueContext != null ? returnValueContext.isCascading() : false,
        returnValueContext != null ? returnValueContext.unwrapMode() : UnwrapMode.AUTOMATIC);
  }

  private List<ConstrainedParameter> getParameters(
      ConstraintHelper constraintHelper, ParameterNameProvider parameterNameProvider) {
    List<ConstrainedParameter> constrainedParameters = newArrayList();

    for (int i = 0; i < parameterContexts.length; i++) {
      ParameterConstraintMappingContextImpl parameter = parameterContexts[i];
      if (parameter != null) {
        constrainedParameters.add(parameter.build(constraintHelper, parameterNameProvider));
      } else {
        constrainedParameters.add(
            new ConstrainedParameter(
                ConfigurationSource.API,
                ConstraintLocation.forParameter(executable, i),
                ReflectionHelper.typeOf(executable, i),
                i,
                executable.getParameterNames(parameterNameProvider).get(i)));
      }
    }

    return constrainedParameters;
  }
}
/** @author Hardy Ferentschik */
public class XmlMappingParser {

  private static final Log log = LoggerFactory.make();
  private static final String MESSAGE_PARAM = "message";
  private static final String GROUPS_PARAM = "groups";
  private static final String PAYLOAD_PARAM = "payload";
  private static final String PACKAGE_SEPARATOR = ".";

  private final Set<Class<?>> processedClasses = newHashSet();
  private final ConstraintHelper constraintHelper;
  private final AnnotationProcessingOptions annotationProcessingOptions;
  private final Map<Class<?>, Set<MetaConstraint<?>>> constraintMap;
  private final Map<Class<?>, List<Member>> cascadedMembers;
  private final Map<Class<?>, List<Class<?>>> defaultSequences;

  private final XmlParserHelper xmlParserHelper = new XmlParserHelper();

  private static final ConcurrentMap<String, String> SCHEMAS_BY_VERSION =
      new ConcurrentHashMap<String, String>(2, 0.75f, 1);

  static {
    SCHEMAS_BY_VERSION.put("1.0", "META-INF/validation-mapping-1.0.xsd");
    SCHEMAS_BY_VERSION.put("1.1", "META-INF/validation-mapping-1.1.xsd");
  }

  public XmlMappingParser(ConstraintHelper constraintHelper) {
    this.constraintHelper = constraintHelper;
    this.annotationProcessingOptions = new AnnotationProcessingOptions();
    this.constraintMap = newHashMap();
    this.cascadedMembers = newHashMap();
    this.defaultSequences = newHashMap();
  }

  /**
   * Parses the given set of input stream representing XML constraint mappings.
   *
   * @param mappingStreams The streams to parse. Must support the mark/reset contract.
   */
  public final void parse(Set<InputStream> mappingStreams) {

    try {
      JAXBContext jc = JAXBContext.newInstance(ConstraintMappingsType.class);

      for (InputStream in : mappingStreams) {

        String schemaVersion = xmlParserHelper.getSchemaVersion("constraint mapping file", in);
        String schemaResourceName = getSchemaResourceName(schemaVersion);
        Schema schema = xmlParserHelper.getSchema(schemaResourceName);

        Unmarshaller unmarshaller = jc.createUnmarshaller();
        unmarshaller.setSchema(schema);

        ConstraintMappingsType mapping = getValidationConfig(in, unmarshaller);
        String defaultPackage = mapping.getDefaultPackage();

        parseConstraintDefinitions(mapping.getConstraintDefinition(), defaultPackage);

        for (BeanType bean : mapping.getBean()) {
          Class<?> beanClass = getClass(bean.getClazz(), defaultPackage);
          checkClassHasNotBeenProcessed(processedClasses, beanClass);
          annotationProcessingOptions.ignoreAnnotationConstraintForClass(
              beanClass, bean.getIgnoreAnnotations());
          parseClassLevelOverrides(bean.getClassType(), beanClass, defaultPackage);
          parseFieldLevelOverrides(bean.getField(), beanClass, defaultPackage);
          parsePropertyLevelOverrides(bean.getGetter(), beanClass, defaultPackage);
          processedClasses.add(beanClass);
        }
      }
    } catch (JAXBException e) {
      throw log.getErrorParsingMappingFileException(e);
    }
  }

  public final Set<Class<?>> getXmlConfiguredClasses() {
    return processedClasses;
  }

  public final AnnotationProcessingOptions getAnnotationProcessingOptions() {
    return annotationProcessingOptions;
  }

  public final <T> Set<MetaConstraint<?>> getConstraintsForClass(Class<T> beanClass) {

    Set<MetaConstraint<?>> theValue = constraintMap.get(beanClass);

    return theValue != null ? theValue : Collections.<MetaConstraint<?>>emptySet();
  }

  public final List<Member> getCascadedMembersForClass(Class<?> beanClass) {
    if (cascadedMembers.containsKey(beanClass)) {
      return cascadedMembers.get(beanClass);
    } else {
      return Collections.emptyList();
    }
  }

  public final List<Class<?>> getDefaultSequenceForClass(Class<?> beanClass) {
    return defaultSequences.get(beanClass);
  }

  @SuppressWarnings("unchecked")
  private void parseConstraintDefinitions(
      List<ConstraintDefinitionType> constraintDefinitionList, String defaultPackage) {
    for (ConstraintDefinitionType constraintDefinition : constraintDefinitionList) {
      String annotationClassName = constraintDefinition.getAnnotation();

      Class<?> clazz = getClass(annotationClassName, defaultPackage);
      if (!clazz.isAnnotation()) {
        throw log.getIsNotAnAnnotationException(annotationClassName);
      }
      Class<? extends Annotation> annotationClass = (Class<? extends Annotation>) clazz;

      ValidatedByType validatedByType = constraintDefinition.getValidatedBy();
      List<Class<? extends ConstraintValidator<? extends Annotation, ?>>>
          constraintValidatorClasses = newArrayList();
      if (validatedByType.getIncludeExistingValidators() != null
          && validatedByType.getIncludeExistingValidators()) {
        constraintValidatorClasses.addAll(findConstraintValidatorClasses(annotationClass));
      }
      for (String validatorClassName : validatedByType.getValue()) {
        Class<? extends ConstraintValidator<?, ?>> validatorClass;
        validatorClass =
            (Class<? extends ConstraintValidator<?, ?>>)
                ReflectionHelper.loadClass(validatorClassName, this.getClass());

        if (!ConstraintValidator.class.isAssignableFrom(validatorClass)) {
          throw log.getIsNotAConstraintValidatorClassException(validatorClass);
        }

        constraintValidatorClasses.add(validatorClass);
      }
      constraintHelper.addConstraintValidatorDefinition(
          annotationClass, constraintValidatorClasses);
    }
  }

  private List<Class<? extends ConstraintValidator<? extends Annotation, ?>>>
      findConstraintValidatorClasses(Class<? extends Annotation> annotationType) {
    List<Class<? extends ConstraintValidator<? extends Annotation, ?>>>
        constraintValidatorDefinitionClasses = newArrayList();
    if (constraintHelper.isBuiltinConstraint(annotationType)) {
      constraintValidatorDefinitionClasses.addAll(
          constraintHelper.getBuiltInConstraints(annotationType));
    } else {
      Class<? extends ConstraintValidator<?, ?>>[] validatedBy =
          annotationType.getAnnotation(Constraint.class).validatedBy();
      constraintValidatorDefinitionClasses.addAll(Arrays.asList(validatedBy));
    }
    return constraintValidatorDefinitionClasses;
  }

  private void checkClassHasNotBeenProcessed(Set<Class<?>> processedClasses, Class<?> beanClass) {
    if (processedClasses.contains(beanClass)) {
      throw log.getBeanClassHasAlreadyBeConfiguredInXmlException(beanClass.getName());
    }
  }

  private void parseFieldLevelOverrides(
      List<FieldType> fields, Class<?> beanClass, String defaultPackage) {
    List<String> fieldNames = newArrayList();
    for (FieldType fieldType : fields) {
      String fieldName = fieldType.getName();
      if (fieldNames.contains(fieldName)) {
        throw log.getIsDefinedTwiceInMappingXmlForBeanException(fieldName, beanClass.getName());
      } else {
        fieldNames.add(fieldName);
      }
      final boolean containsField = ReflectionHelper.containsDeclaredField(beanClass, fieldName);
      if (!containsField) {
        throw log.getBeanDoesNotContainTheFieldException(beanClass.getName(), fieldName);
      }
      final Field field = ReflectionHelper.getDeclaredField(beanClass, fieldName);

      // ignore annotations
      boolean ignoreFieldAnnotation =
          fieldType.getIgnoreAnnotations() == null ? false : fieldType.getIgnoreAnnotations();
      if (ignoreFieldAnnotation) {
        annotationProcessingOptions.ignorePropertyLevelConstraintAnnotationsOnMember(field);
      }

      // valid
      if (fieldType.getValid() != null) {
        addCascadedMember(beanClass, field);
      }

      // constraints
      for (ConstraintType constraint : fieldType.getConstraint()) {
        MetaConstraint<?> metaConstraint =
            createMetaConstraint(constraint, beanClass, field, defaultPackage);
        addMetaConstraint(beanClass, metaConstraint);
      }
    }
  }

  private void parsePropertyLevelOverrides(
      List<GetterType> getters, Class<?> beanClass, String defaultPackage) {
    List<String> getterNames = newArrayList();
    for (GetterType getterType : getters) {
      String getterName = getterType.getName();
      if (getterNames.contains(getterName)) {
        throw log.getIsDefinedTwiceInMappingXmlForBeanException(getterName, beanClass.getName());
      } else {
        getterNames.add(getterName);
      }
      boolean containsMethod =
          ReflectionHelper.containsMethodWithPropertyName(beanClass, getterName);
      if (!containsMethod) {
        throw log.getBeanDoesNotContainThePropertyException(beanClass.getName(), getterName);
      }
      final Method method = ReflectionHelper.getMethodFromPropertyName(beanClass, getterName);

      // ignore annotations
      boolean ignoreGetterAnnotation =
          getterType.getIgnoreAnnotations() == null ? false : getterType.getIgnoreAnnotations();
      if (ignoreGetterAnnotation) {
        annotationProcessingOptions.ignorePropertyLevelConstraintAnnotationsOnMember(method);
      }

      // valid
      if (getterType.getValid() != null) {
        addCascadedMember(beanClass, method);
      }

      // constraints
      for (ConstraintType constraint : getterType.getConstraint()) {
        MetaConstraint<?> metaConstraint =
            createMetaConstraint(constraint, beanClass, method, defaultPackage);
        addMetaConstraint(beanClass, metaConstraint);
      }
    }
  }

  private void parseClassLevelOverrides(
      ClassType classType, Class<?> beanClass, String defaultPackage) {
    if (classType == null) {
      return;
    }

    // ignore annotation
    if (classType.getIgnoreAnnotations() != null) {
      annotationProcessingOptions.ignoreClassLevelConstraintAnnotations(
          beanClass, classType.getIgnoreAnnotations());
    }

    // group sequence
    List<Class<?>> groupSequence =
        createGroupSequence(classType.getGroupSequence(), defaultPackage);
    if (!groupSequence.isEmpty()) {
      defaultSequences.put(beanClass, groupSequence);
    }

    // constraints
    for (ConstraintType constraint : classType.getConstraint()) {
      MetaConstraint<?> metaConstraint =
          createMetaConstraint(constraint, beanClass, null, defaultPackage);
      addMetaConstraint(beanClass, metaConstraint);
    }
  }

  private void addMetaConstraint(Class<?> beanClass, MetaConstraint<?> metaConstraint) {
    if (constraintMap.containsKey(beanClass)) {
      constraintMap.get(beanClass).add(metaConstraint);
    } else {
      Set<MetaConstraint<?>> constraintList = newHashSet();
      constraintList.add(metaConstraint);
      constraintMap.put(beanClass, constraintList);
    }
  }

  private void addCascadedMember(Class<?> beanClass, Member member) {
    if (cascadedMembers.containsKey(beanClass)) {
      cascadedMembers.get(beanClass).add(member);
    } else {
      List<Member> tmpList = newArrayList();
      tmpList.add(member);
      cascadedMembers.put(beanClass, tmpList);
    }
  }

  private List<Class<?>> createGroupSequence(
      GroupSequenceType groupSequenceType, String defaultPackage) {
    List<Class<?>> groupSequence = newArrayList();
    if (groupSequenceType != null) {
      for (String groupName : groupSequenceType.getValue()) {
        Class<?> group = getClass(groupName, defaultPackage);
        groupSequence.add(group);
      }
    }
    return groupSequence;
  }

  private <A extends Annotation, T> MetaConstraint<?> createMetaConstraint(
      ConstraintType constraint, Class<T> beanClass, Member member, String defaultPackage) {
    @SuppressWarnings("unchecked")
    Class<A> annotationClass = (Class<A>) getClass(constraint.getAnnotation(), defaultPackage);
    AnnotationDescriptor<A> annotationDescriptor = new AnnotationDescriptor<A>(annotationClass);

    if (constraint.getMessage() != null) {
      annotationDescriptor.setValue(MESSAGE_PARAM, constraint.getMessage());
    }
    annotationDescriptor.setValue(GROUPS_PARAM, getGroups(constraint.getGroups(), defaultPackage));
    annotationDescriptor.setValue(
        PAYLOAD_PARAM, getPayload(constraint.getPayload(), defaultPackage));

    for (ElementType elementType : constraint.getElement()) {
      String name = elementType.getName();
      checkNameIsValid(name);
      Class<?> returnType = getAnnotationParameterType(annotationClass, name);
      Object elementValue = getElementValue(elementType, returnType);
      annotationDescriptor.setValue(name, elementValue);
    }

    A annotation;
    try {
      annotation = AnnotationFactory.create(annotationDescriptor);
    } catch (RuntimeException e) {
      throw log.getUnableToCreateAnnotationForConfiguredConstraintException(e);
    }

    java.lang.annotation.ElementType type = java.lang.annotation.ElementType.TYPE;
    if (member instanceof Method) {
      type = java.lang.annotation.ElementType.METHOD;
    } else if (member instanceof Field) {
      type = java.lang.annotation.ElementType.FIELD;
    }

    // we set initially ConstraintOrigin.DEFINED_LOCALLY for all xml configured constraints
    // later we will make copies of this constraint descriptor when needed and adjust the
    // ConstraintOrigin
    ConstraintDescriptorImpl<A> constraintDescriptor =
        new ConstraintDescriptorImpl<A>(
            annotation, constraintHelper, type, ConstraintOrigin.DEFINED_LOCALLY);

    return new MetaConstraint<A>(
        constraintDescriptor, new BeanConstraintLocation(beanClass, member));
  }

  private <A extends Annotation> Class<?> getAnnotationParameterType(
      Class<A> annotationClass, String name) {
    Method m = ReflectionHelper.getMethod(annotationClass, name);
    if (m == null) {
      throw log.getAnnotationDoesNotContainAParameterException(annotationClass.getName(), name);
    }
    return m.getReturnType();
  }

  private Object getElementValue(ElementType elementType, Class<?> returnType) {
    removeEmptyContentElements(elementType);

    boolean isArray = returnType.isArray();
    if (!isArray) {
      if (elementType.getContent().size() != 1) {
        throw log.getAttemptToSpecifyAnArrayWhereSingleValueIsExpectedException();
      }
      return getSingleValue(elementType.getContent().get(0), returnType);
    } else {
      List<Object> values = newArrayList();
      for (Serializable s : elementType.getContent()) {
        values.add(getSingleValue(s, returnType.getComponentType()));
      }
      return values.toArray(
          (Object[]) Array.newInstance(returnType.getComponentType(), values.size()));
    }
  }

  private void removeEmptyContentElements(ElementType elementType) {
    List<Serializable> contentToDelete = newArrayList();
    for (Serializable content : elementType.getContent()) {
      if (content instanceof String && ((String) content).matches("[\\n ].*")) {
        contentToDelete.add(content);
      }
    }
    elementType.getContent().removeAll(contentToDelete);
  }

  private Object getSingleValue(Serializable serializable, Class<?> returnType) {

    Object returnValue;
    if (serializable instanceof String) {
      String value = (String) serializable;
      returnValue = convertStringToReturnType(returnType, value);
    } else if (serializable instanceof JAXBElement
        && ((JAXBElement<?>) serializable).getDeclaredType().equals(String.class)) {
      JAXBElement<?> elem = (JAXBElement<?>) serializable;
      String value = (String) elem.getValue();
      returnValue = convertStringToReturnType(returnType, value);
    } else if (serializable instanceof JAXBElement
        && ((JAXBElement<?>) serializable).getDeclaredType().equals(AnnotationType.class)) {
      JAXBElement<?> elem = (JAXBElement<?>) serializable;
      AnnotationType annotationType = (AnnotationType) elem.getValue();
      try {
        @SuppressWarnings("unchecked")
        Class<Annotation> annotationClass = (Class<Annotation>) returnType;
        returnValue = createAnnotation(annotationType, annotationClass);
      } catch (ClassCastException e) {
        throw log.getUnexpectedParameterValueException(e);
      }
    } else {
      throw log.getUnexpectedParameterValueException();
    }
    return returnValue;
  }

  private <A extends Annotation> Annotation createAnnotation(
      AnnotationType annotationType, Class<A> returnType) {
    AnnotationDescriptor<A> annotationDescriptor = new AnnotationDescriptor<A>(returnType);
    for (ElementType elementType : annotationType.getElement()) {
      String name = elementType.getName();
      Class<?> parameterType = getAnnotationParameterType(returnType, name);
      Object elementValue = getElementValue(elementType, parameterType);
      annotationDescriptor.setValue(name, elementValue);
    }
    return AnnotationFactory.create(annotationDescriptor);
  }

  private Object convertStringToReturnType(Class<?> returnType, String value) {
    Object returnValue;
    if (returnType.getName().equals(byte.class.getName())) {
      try {
        returnValue = Byte.parseByte(value);
      } catch (NumberFormatException e) {
        throw log.getInvalidNumberFormatException("byte", e);
      }
    } else if (returnType.getName().equals(short.class.getName())) {
      try {
        returnValue = Short.parseShort(value);
      } catch (NumberFormatException e) {
        throw log.getInvalidNumberFormatException("short", e);
      }
    } else if (returnType.getName().equals(int.class.getName())) {
      try {
        returnValue = Integer.parseInt(value);
      } catch (NumberFormatException e) {
        throw log.getInvalidNumberFormatException("int", e);
      }
    } else if (returnType.getName().equals(long.class.getName())) {
      try {
        returnValue = Long.parseLong(value);
      } catch (NumberFormatException e) {
        throw log.getInvalidNumberFormatException("long", e);
      }
    } else if (returnType.getName().equals(float.class.getName())) {
      try {
        returnValue = Float.parseFloat(value);
      } catch (NumberFormatException e) {
        throw log.getInvalidNumberFormatException("float", e);
      }
    } else if (returnType.getName().equals(double.class.getName())) {
      try {
        returnValue = Double.parseDouble(value);
      } catch (NumberFormatException e) {
        throw log.getInvalidNumberFormatException("double", e);
      }
    } else if (returnType.getName().equals(boolean.class.getName())) {
      returnValue = Boolean.parseBoolean(value);
    } else if (returnType.getName().equals(char.class.getName())) {
      if (value.length() != 1) {
        throw log.getInvalidCharValueException(value);
      }
      returnValue = value.charAt(0);
    } else if (returnType.getName().equals(String.class.getName())) {
      returnValue = value;
    } else if (returnType.getName().equals(Class.class.getName())) {
      returnValue = ReflectionHelper.loadClass(value, this.getClass());
    } else {
      try {
        @SuppressWarnings("unchecked")
        Class<Enum> enumClass = (Class<Enum>) returnType;
        returnValue = Enum.valueOf(enumClass, value);
      } catch (ClassCastException e) {
        throw log.getInvalidReturnTypeException(returnType, e);
      }
    }
    return returnValue;
  }

  private void checkNameIsValid(String name) {
    if (MESSAGE_PARAM.equals(name) || GROUPS_PARAM.equals(name)) {
      throw log.getReservedParameterNamesException(MESSAGE_PARAM, GROUPS_PARAM, PAYLOAD_PARAM);
    }
  }

  private Class<?>[] getGroups(GroupsType groupsType, String defaultPackage) {
    if (groupsType == null) {
      return new Class[] {};
    }

    List<Class<?>> groupList = newArrayList();
    for (String groupClass : groupsType.getValue()) {
      groupList.add(getClass(groupClass, defaultPackage));
    }
    return groupList.toArray(new Class[groupList.size()]);
  }

  @SuppressWarnings("unchecked")
  private Class<? extends Payload>[] getPayload(PayloadType payloadType, String defaultPackage) {
    if (payloadType == null) {
      return new Class[] {};
    }

    List<Class<? extends Payload>> payloadList = newArrayList();
    for (String groupClass : payloadType.getValue()) {
      Class<?> payload = getClass(groupClass, defaultPackage);
      if (!Payload.class.isAssignableFrom(payload)) {
        throw log.getWrongPayloadClassException(payload.getName());
      } else {
        payloadList.add((Class<? extends Payload>) payload);
      }
    }
    return payloadList.toArray(new Class[payloadList.size()]);
  }

  private Class<?> getClass(String clazz, String defaultPackage) {
    String fullyQualifiedClass;
    if (isQualifiedClass(clazz)) {
      fullyQualifiedClass = clazz;
    } else {
      fullyQualifiedClass = defaultPackage + PACKAGE_SEPARATOR + clazz;
    }
    return ReflectionHelper.loadClass(fullyQualifiedClass, this.getClass());
  }

  private boolean isQualifiedClass(String clazz) {
    return clazz.contains(PACKAGE_SEPARATOR);
  }

  private ConstraintMappingsType getValidationConfig(InputStream in, Unmarshaller unmarshaller) {
    ConstraintMappingsType constraintMappings;
    try {
      // check whether mark is supported, if so we can reset the stream in order to allow reuse of
      // Configuration
      boolean markSupported = in.markSupported();
      if (markSupported) {
        in.mark(Integer.MAX_VALUE);
      }

      StreamSource stream = new StreamSource(new CloseIgnoringInputStream(in));
      JAXBElement<ConstraintMappingsType> root =
          unmarshaller.unmarshal(stream, ConstraintMappingsType.class);
      constraintMappings = root.getValue();

      if (markSupported) {
        try {
          in.reset();
        } catch (IOException e) {
          log.debug("Unable to reset input stream.");
        }
      }
    } catch (JAXBException e) {
      throw log.getErrorParsingMappingFileException(e);
    }
    return constraintMappings;
  }

  private String getSchemaResourceName(String schemaVersion) {
    String schemaResource = SCHEMAS_BY_VERSION.get(schemaVersion);

    if (schemaResource == null) {
      throw log.getUnsupportedSchemaVersionException("constraint mapping file", schemaVersion);
    }

    return schemaResource;
  }

  // JAXB closes the underlying input stream
  private static class CloseIgnoringInputStream extends FilterInputStream {
    public CloseIgnoringInputStream(InputStream in) {
      super(in);
    }

    @Override
    public void close() {
      // do nothing
    }
  }
}
/**
 * This class encapsulates all meta data needed for validation. Implementations of {@code Validator}
 * interface can instantiate an instance of this class and delegate the metadata extraction to it.
 *
 * @author Hardy Ferentschik
 * @author Gunnar Morling
 * @author Kevin Pollet <*****@*****.**> (C) 2011 SERLI
 */
public final class BeanMetaDataImpl<T> implements BeanMetaData<T> {

  private static final Log log = LoggerFactory.make();

  /** The root bean class for this meta data. */
  private final Class<T> beanClass;

  /**
   * Set of all constraints for this bean type (defined on any implemented interfaces or super
   * types)
   */
  private final Set<MetaConstraint<?>> allMetaConstraints;

  /**
   * Set of all constraints which are directly defined on the bean or any of the directly
   * implemented interfaces
   */
  private final Set<MetaConstraint<?>> directMetaConstraints;

  /**
   * Contains constrained related meta data for all methods and constructors of the type represented
   * by this bean meta data. Keyed by executable, values are an aggregated view on each executable
   * together with all the executables from the inheritance hierarchy with the same signature.
   */
  private final Map<String, ExecutableMetaData> executableMetaDataMap;

  /** Property meta data keyed against the property name */
  private final Map<String, PropertyMetaData> propertyMetaDataMap;

  /** The cascaded properties of this bean. */
  private final Set<Cascadable> cascadedProperties;

  /** The bean descriptor for this bean. */
  private final BeanDescriptor beanDescriptor;

  /** The default groups sequence for this bean class. */
  private List<Class<?>> defaultGroupSequence = newArrayList();

  /**
   * The default group sequence provider.
   *
   * @see org.hibernate.validator.group.GroupSequenceProvider
   * @see DefaultGroupSequenceProvider
   */
  private DefaultGroupSequenceProvider<? super T> defaultGroupSequenceProvider;

  /**
   * The class hierarchy for this class starting with the class itself going up the inheritance
   * chain. Interfaces are not included.
   */
  private final List<Class<? super T>> classHierarchyWithoutInterfaces;

  /**
   * Creates a new {@link BeanMetaDataImpl}
   *
   * @param beanClass The Java type represented by this meta data object.
   * @param defaultGroupSequence The default group sequence.
   * @param defaultGroupSequenceProvider The default group sequence provider if set.
   * @param constraintMetaDataSet All constraint meta data relating to the represented type.
   */
  public BeanMetaDataImpl(
      Class<T> beanClass,
      List<Class<?>> defaultGroupSequence,
      DefaultGroupSequenceProvider<? super T> defaultGroupSequenceProvider,
      Set<ConstraintMetaData> constraintMetaDataSet) {

    this.beanClass = beanClass;
    this.propertyMetaDataMap = newHashMap();

    Set<PropertyMetaData> propertyMetaDataSet = newHashSet();
    Set<ExecutableMetaData> executableMetaDataSet = newHashSet();

    for (ConstraintMetaData constraintMetaData : constraintMetaDataSet) {
      if (constraintMetaData.getKind() == ElementKind.PROPERTY) {
        propertyMetaDataSet.add((PropertyMetaData) constraintMetaData);
      } else {
        executableMetaDataSet.add((ExecutableMetaData) constraintMetaData);
      }
    }

    Set<Cascadable> cascadedProperties = newHashSet();
    Set<MetaConstraint<?>> allMetaConstraints = newHashSet();

    for (PropertyMetaData propertyMetaData : propertyMetaDataSet) {
      propertyMetaDataMap.put(propertyMetaData.getName(), propertyMetaData);

      if (propertyMetaData.isCascading()) {
        cascadedProperties.add(propertyMetaData);
      }

      allMetaConstraints.addAll(propertyMetaData.getConstraints());
    }

    this.cascadedProperties = Collections.unmodifiableSet(cascadedProperties);
    this.allMetaConstraints = Collections.unmodifiableSet(allMetaConstraints);

    this.classHierarchyWithoutInterfaces =
        ClassHierarchyHelper.getHierarchy(beanClass, Filters.excludeInterfaces());

    setDefaultGroupSequenceOrProvider(defaultGroupSequence, defaultGroupSequenceProvider);

    this.directMetaConstraints = getDirectConstraints();

    this.executableMetaDataMap = Collections.unmodifiableMap(byIdentifier(executableMetaDataSet));

    this.beanDescriptor =
        new BeanDescriptorImpl(
            beanClass,
            getClassLevelConstraintsAsDescriptors(),
            getConstrainedPropertiesAsDescriptors(),
            getConstrainedMethodsAsDescriptors(),
            getConstrainedConstructorsAsDescriptors(),
            defaultGroupSequenceIsRedefined(),
            getDefaultGroupSequence(null));
  }

  @Override
  public Class<T> getBeanClass() {
    return beanClass;
  }

  @Override
  public boolean hasConstraints() {
    if (beanDescriptor.isBeanConstrained()
        || !beanDescriptor.getConstrainedConstructors().isEmpty()
        || !beanDescriptor
            .getConstrainedMethods(MethodType.NON_GETTER, MethodType.GETTER)
            .isEmpty()) {
      return true;
    }

    return false;
  }

  @Override
  public BeanDescriptor getBeanDescriptor() {
    return beanDescriptor;
  }

  @Override
  public Set<Cascadable> getCascadables() {
    return cascadedProperties;
  }

  @Override
  public PropertyMetaData getMetaDataFor(String propertyName) {
    return propertyMetaDataMap.get(propertyName);
  }

  @Override
  public Set<MetaConstraint<?>> getMetaConstraints() {
    return allMetaConstraints;
  }

  @Override
  public Set<MetaConstraint<?>> getDirectMetaConstraints() {
    return directMetaConstraints;
  }

  @Override
  public ExecutableMetaData getMetaDataFor(ExecutableElement executable) {
    return executableMetaDataMap.get(executable.getIdentifier());
  }

  @Override
  public List<Class<?>> getDefaultGroupSequence(T beanState) {
    if (hasDefaultGroupSequenceProvider()) {
      List<Class<?>> providerDefaultGroupSequence =
          defaultGroupSequenceProvider.getValidationGroups(beanState);
      return getValidDefaultGroupSequence(providerDefaultGroupSequence);
    }

    return Collections.unmodifiableList(defaultGroupSequence);
  }

  @Override
  public boolean defaultGroupSequenceIsRedefined() {
    return defaultGroupSequence.size() > 1 || hasDefaultGroupSequenceProvider();
  }

  @Override
  public List<Class<? super T>> getClassHierarchy() {
    return classHierarchyWithoutInterfaces;
  }

  private Set<ConstraintDescriptorImpl<?>> getClassLevelConstraintsAsDescriptors() {
    Set<MetaConstraint<?>> classLevelConstraints = getClassLevelConstraints(allMetaConstraints);

    Set<ConstraintDescriptorImpl<?>> theValue = newHashSet();

    for (MetaConstraint<?> metaConstraint : classLevelConstraints) {
      theValue.add(metaConstraint.getDescriptor());
    }

    return theValue;
  }

  private Map<String, PropertyDescriptor> getConstrainedPropertiesAsDescriptors() {
    Map<String, PropertyDescriptor> theValue = newHashMap();

    for (Entry<String, PropertyMetaData> entry : propertyMetaDataMap.entrySet()) {
      if (entry.getValue().isConstrained() && entry.getValue().getName() != null) {
        theValue.put(
            entry.getKey(),
            entry
                .getValue()
                .asDescriptor(defaultGroupSequenceIsRedefined(), getDefaultGroupSequence(null)));
      }
    }

    return theValue;
  }

  private Map<String, ExecutableDescriptorImpl> getConstrainedMethodsAsDescriptors() {
    Map<String, ExecutableDescriptorImpl> constrainedMethodDescriptors = newHashMap();

    for (ExecutableMetaData executableMetaData : executableMetaDataMap.values()) {
      if (executableMetaData.getKind() == ElementKind.METHOD
          && executableMetaData.isConstrained()) {
        constrainedMethodDescriptors.put(
            executableMetaData.getIdentifier(),
            executableMetaData.asDescriptor(
                defaultGroupSequenceIsRedefined(), getDefaultGroupSequence(null)));
      }
    }

    return constrainedMethodDescriptors;
  }

  private Map<String, ConstructorDescriptor> getConstrainedConstructorsAsDescriptors() {
    Map<String, ConstructorDescriptor> constrainedMethodDescriptors = newHashMap();

    for (ExecutableMetaData executableMetaData : executableMetaDataMap.values()) {
      if (executableMetaData.getKind() == ElementKind.CONSTRUCTOR
          && executableMetaData.isConstrained()) {
        constrainedMethodDescriptors.put(
            executableMetaData.getIdentifier(),
            executableMetaData.asDescriptor(
                defaultGroupSequenceIsRedefined(), getDefaultGroupSequence(null)));
      }
    }

    return constrainedMethodDescriptors;
  }

  private void setDefaultGroupSequenceOrProvider(
      List<Class<?>> defaultGroupSequence,
      DefaultGroupSequenceProvider<? super T> defaultGroupSequenceProvider) {
    if (defaultGroupSequence != null && defaultGroupSequenceProvider != null) {
      throw log.getInvalidDefaultGroupSequenceDefinitionException();
    }

    if (defaultGroupSequenceProvider != null) {
      this.defaultGroupSequenceProvider = defaultGroupSequenceProvider;
    } else if (defaultGroupSequence != null && !defaultGroupSequence.isEmpty()) {
      setDefaultGroupSequence(defaultGroupSequence);
    } else {
      setDefaultGroupSequence(Arrays.<Class<?>>asList(beanClass));
    }
  }

  private Set<MetaConstraint<?>> getClassLevelConstraints(Set<MetaConstraint<?>> constraints) {
    Set<MetaConstraint<?>> classLevelConstraints =
        partition(constraints, byElementType()).get(ElementType.TYPE);

    return classLevelConstraints != null
        ? classLevelConstraints
        : Collections.<MetaConstraint<?>>emptySet();
  }

  private Set<MetaConstraint<?>> getDirectConstraints() {
    Set<MetaConstraint<?>> constraints = newHashSet();

    Set<Class<?>> classAndInterfaces = newHashSet();
    classAndInterfaces.add(beanClass);
    classAndInterfaces.addAll(ClassHierarchyHelper.getDirectlyImplementedInterfaces(beanClass));

    for (Class<?> clazz : classAndInterfaces) {
      for (MetaConstraint<?> metaConstraint : allMetaConstraints) {
        if (metaConstraint.getLocation().getDeclaringClass().equals(clazz)) {
          constraints.add(metaConstraint);
        }
      }
    }

    return Collections.unmodifiableSet(constraints);
  }

  /** Builds up the method meta data for this type */
  private Map<String, ExecutableMetaData> byIdentifier(Set<ExecutableMetaData> executables) {
    Map<String, ExecutableMetaData> theValue = newHashMap();

    for (ExecutableMetaData executableMetaData : executables) {
      theValue.put(executableMetaData.getIdentifier(), executableMetaData);
    }

    return theValue;
  }

  private void setDefaultGroupSequence(List<Class<?>> groupSequence) {
    defaultGroupSequence = getValidDefaultGroupSequence(groupSequence);
  }

  private List<Class<?>> getValidDefaultGroupSequence(List<Class<?>> groupSequence) {
    List<Class<?>> validDefaultGroupSequence = new ArrayList<Class<?>>();

    boolean groupSequenceContainsDefault = false;
    if (groupSequence != null) {
      for (Class<?> group : groupSequence) {
        if (group.getName().equals(beanClass.getName())) {
          validDefaultGroupSequence.add(Default.class);
          groupSequenceContainsDefault = true;
        } else if (group.getName().equals(Default.class.getName())) {
          throw log.getNoDefaultGroupInGroupSequenceException();
        } else {
          validDefaultGroupSequence.add(group);
        }
      }
    }
    if (!groupSequenceContainsDefault) {
      throw log.getBeanClassMustBePartOfRedefinedDefaultGroupSequenceException(beanClass.getName());
    }
    if (log.isTraceEnabled()) {
      log.tracef(
          "Members of the default group sequence for bean %s are: %s.",
          beanClass.getName(), validDefaultGroupSequence);
    }

    return validDefaultGroupSequence;
  }

  private boolean hasDefaultGroupSequenceProvider() {
    return defaultGroupSequenceProvider != null;
  }

  private Partitioner<ElementType, MetaConstraint<?>> byElementType() {
    return new Partitioner<ElementType, MetaConstraint<?>>() {
      @Override
      public ElementType getPartition(MetaConstraint<?> constraint) {
        return constraint.getElementType();
      }
    };
  }

  @Override
  public String toString() {
    final StringBuilder sb = new StringBuilder();
    sb.append("BeanMetaDataImpl");
    sb.append("{beanClass=").append(beanClass.getSimpleName());
    sb.append(", constraintCount=").append(getMetaConstraints().size());
    sb.append(", cascadedPropertiesCount=").append(cascadedProperties.size());
    sb.append(", defaultGroupSequence=").append(getDefaultGroupSequence(null));
    sb.append('}');
    return sb.toString();
  }

  public static class BeanMetaDataBuilder<T> {

    private final ConstraintHelper constraintHelper;

    private final Class<T> beanClass;

    private final Set<BuilderDelegate> builders = newHashSet();

    private final ExecutableHelper executableHelper;

    private ConfigurationSource sequenceSource;

    private ConfigurationSource providerSource;

    private List<Class<?>> defaultGroupSequence;

    private DefaultGroupSequenceProvider<? super T> defaultGroupSequenceProvider;

    private BeanMetaDataBuilder(
        ConstraintHelper constraintHelper, ExecutableHelper executableHelper, Class<T> beanClass) {
      this.beanClass = beanClass;
      this.constraintHelper = constraintHelper;
      this.executableHelper = executableHelper;
    }

    public static <T> BeanMetaDataBuilder<T> getInstance(
        ConstraintHelper constraintHelper, ExecutableHelper executableHelper, Class<T> beanClass) {
      return new BeanMetaDataBuilder<T>(constraintHelper, executableHelper, beanClass);
    }

    public void add(BeanConfiguration<? super T> configuration) {
      if (configuration.getBeanClass().equals(beanClass)) {
        if (configuration.getDefaultGroupSequence() != null
            && (sequenceSource == null
                || configuration.getSource().getPriority() >= sequenceSource.getPriority())) {

          sequenceSource = configuration.getSource();
          defaultGroupSequence = configuration.getDefaultGroupSequence();
        }

        if (configuration.getDefaultGroupSequenceProvider() != null
            && (providerSource == null
                || configuration.getSource().getPriority() >= providerSource.getPriority())) {

          providerSource = configuration.getSource();
          defaultGroupSequenceProvider = configuration.getDefaultGroupSequenceProvider();
        }
      }

      for (ConstrainedElement constrainedElement : configuration.getConstrainedElements()) {
        addMetaDataToBuilder(constrainedElement, builders);
      }
    }

    private void addMetaDataToBuilder(
        ConstrainedElement constrainableElement, Set<BuilderDelegate> builders) {
      for (BuilderDelegate builder : builders) {
        boolean foundBuilder = builder.add(constrainableElement);

        if (foundBuilder) {
          return;
        }
      }

      builders.add(
          new BuilderDelegate(beanClass, constrainableElement, constraintHelper, executableHelper));
    }

    public BeanMetaDataImpl<T> build() {
      Set<ConstraintMetaData> aggregatedElements = newHashSet();

      for (BuilderDelegate builder : builders) {
        aggregatedElements.addAll(builder.build());
      }

      return new BeanMetaDataImpl<T>(
          beanClass, defaultGroupSequence, defaultGroupSequenceProvider, aggregatedElements);
    }
  }

  private static class BuilderDelegate {
    private final Class<?> beanClass;
    private final ConstraintHelper constraintHelper;
    private final ExecutableHelper executableHelper;
    private MetaDataBuilder propertyBuilder;
    private ExecutableMetaData.Builder methodBuilder;

    public BuilderDelegate(
        Class<?> beanClass,
        ConstrainedElement constrainedElement,
        ConstraintHelper constraintHelper,
        ExecutableHelper executableHelper) {
      this.beanClass = beanClass;
      this.constraintHelper = constraintHelper;
      this.executableHelper = executableHelper;

      switch (constrainedElement.getKind()) {
        case FIELD:
          ConstrainedField constrainedField = (ConstrainedField) constrainedElement;
          propertyBuilder =
              new PropertyMetaData.Builder(beanClass, constrainedField, constraintHelper);
          break;
        case CONSTRUCTOR:
        case METHOD:
          ConstrainedExecutable constrainedExecutable = (ConstrainedExecutable) constrainedElement;
          methodBuilder =
              new ExecutableMetaData.Builder(
                  beanClass, constrainedExecutable, constraintHelper, executableHelper);

          if (constrainedExecutable.isGetterMethod()) {
            propertyBuilder =
                new PropertyMetaData.Builder(beanClass, constrainedExecutable, constraintHelper);
          }
          break;
        case TYPE:
          ConstrainedType constrainedType = (ConstrainedType) constrainedElement;
          propertyBuilder =
              new PropertyMetaData.Builder(beanClass, constrainedType, constraintHelper);
          break;
      }
    }

    public boolean add(ConstrainedElement constrainedElement) {
      boolean added = false;

      if (methodBuilder != null && methodBuilder.accepts(constrainedElement)) {
        methodBuilder.add(constrainedElement);
        added = true;
      }

      if (propertyBuilder != null && propertyBuilder.accepts(constrainedElement)) {
        propertyBuilder.add(constrainedElement);

        if (!added
            && constrainedElement.getKind() == ConstrainedElementKind.METHOD
            && methodBuilder == null) {
          ConstrainedExecutable constrainedMethod = (ConstrainedExecutable) constrainedElement;
          methodBuilder =
              new ExecutableMetaData.Builder(
                  beanClass, constrainedMethod, constraintHelper, executableHelper);
        }

        added = true;
      }

      return added;
    }

    public Set<ConstraintMetaData> build() {
      Set<ConstraintMetaData> metaDataSet = newHashSet();

      if (propertyBuilder != null) {
        metaDataSet.add(propertyBuilder.build());
      }

      if (methodBuilder != null) {
        metaDataSet.add(methodBuilder.build());
      }

      return metaDataSet;
    }
  }
}
/**
 * A message interpolator which can interpolate the validated value and format this value using the
 * syntax from {@link java.util.Formatter}. Check the <code>Formatter</code> documentation for
 * formatting syntax and options. If no formatting string is specified <code>
 * String.valueOf(validatedValue)}</code> is called.
 *
 * <p>To interpolate the validated value add <code>&#123;validatedValue&#125;</code> into the
 * message. To specify a format pattern use <code>$&#123;validatedValue:[format string]&#125;</code>
 * , e.g. <code>$&#123;validatedValue:%1$ty&#125;</code>.
 *
 * @author Hardy Ferentschik
 */
public class ValueFormatterMessageInterpolator implements MessageInterpolator {
  private static final Log log = LoggerFactory.make();

  public static final String VALIDATED_VALUE_KEYWORD = "validatedValue";
  public static final String VALIDATED_VALUE_FORMAT_SEPARATOR = ":";

  private static final Pattern VALIDATED_VALUE_START_PATTERN =
      Pattern.compile("\\$\\{" + VALIDATED_VALUE_KEYWORD);
  private final MessageInterpolator delegate;
  private final Locale defaultLocale;

  public ValueFormatterMessageInterpolator() {
    this(null);
  }

  public ValueFormatterMessageInterpolator(MessageInterpolator userMessageInterpolator) {
    defaultLocale = Locale.getDefault();
    if (userMessageInterpolator == null) {
      this.delegate = new ResourceBundleMessageInterpolator();
    } else {
      this.delegate = userMessageInterpolator;
    }
  }

  public String interpolate(String message, Context context) {
    return interpolate(message, context, defaultLocale);
  }

  public String interpolate(String message, Context context, Locale locale) {
    String tmp = delegate.interpolate(message, context, locale);
    return interpolateMessage(tmp, context.getValidatedValue(), locale);
  }

  /**
   * Interpolate the validated value in the given message.
   *
   * @param message the message where validated value have to be interpolated
   * @param validatedValue the value of the object being validated
   * @param locale the {@code Locale} to use for message interpolation
   * @return the interpolated message
   */
  private String interpolateMessage(String message, Object validatedValue, Locale locale) {
    String interpolatedMessage = message;
    Matcher matcher = VALIDATED_VALUE_START_PATTERN.matcher(message);

    while (matcher.find()) {
      int nbOpenCurlyBrace = 1;
      boolean isDoubleQuoteBloc = false;
      boolean isSimpleQuoteBloc = false;
      int lastIndex = matcher.end();

      do {
        char current = message.charAt(lastIndex);

        if (current == '\'') {
          if (!isDoubleQuoteBloc && !isEscaped(message, lastIndex)) {
            isSimpleQuoteBloc = !isSimpleQuoteBloc;
          }
        } else if (current == '"') {
          if (!isSimpleQuoteBloc && !isEscaped(message, lastIndex)) {
            isDoubleQuoteBloc = !isDoubleQuoteBloc;
          }
        } else if (!isDoubleQuoteBloc && !isSimpleQuoteBloc) {
          if (current == '{') {
            nbOpenCurlyBrace++;
          } else if (current == '}') {
            nbOpenCurlyBrace--;
          }
        }

        lastIndex++;

      } while (nbOpenCurlyBrace > 0 && lastIndex < message.length());

      // The validated value expression seems correct
      if (nbOpenCurlyBrace == 0) {
        String expression = message.substring(matcher.start(), lastIndex);
        String expressionValue = interpolateValidatedValue(expression, validatedValue, locale);
        interpolatedMessage =
            interpolatedMessage.replaceFirst(
                Pattern.quote(expression), Matcher.quoteReplacement(expressionValue));
      }
    }
    return interpolatedMessage;
  }

  /**
   * Returns if the character at the given index in the String is an escaped character (preceded by
   * a backslash character).
   *
   * @param enclosingString the string which contain the given character
   * @param charIndex the index of the character
   * @return true if the given character is escaped false otherwise
   */
  private boolean isEscaped(String enclosingString, int charIndex) {
    if (charIndex < 0 || charIndex > enclosingString.length()) {
      throw log.getInvalidIndexException("0", "enclosingString.length() - 1");
    }
    return charIndex > 0 && enclosingString.charAt(charIndex - 1) == '\\';
  }

  /**
   * Returns the value of the interpolated validated value.
   *
   * @param expression the expression to interpolate
   * @param validatedValue the value of the object being validated
   * @param locale the {@code Locale} to be used
   * @return the interpolated value
   */
  private String interpolateValidatedValue(
      String expression, Object validatedValue, Locale locale) {
    String interpolatedValue;
    int separatorIndex = expression.indexOf(VALIDATED_VALUE_FORMAT_SEPARATOR);

    if (separatorIndex == -1) {
      interpolatedValue = String.valueOf(validatedValue);
    } else {
      String format = expression.substring(separatorIndex + 1, expression.length() - 1);
      if (format.length() == 0) {
        throw log.getMissingFormatStringInTemplateException(expression);
      }
      try {
        interpolatedValue = String.format(locale, format, validatedValue);
      } catch (IllegalFormatException e) {
        throw log.throwInvalidFormat(e.getMessage(), e);
      }
    }
    return interpolatedValue;
  }
}
/**
 * Provides utility methods for working with types.
 *
 * @author Mark Hobson
 * @author Hardy Ferentschik
 */
public final class TypeHelper {
  private static final Map<Class<?>, Set<Class<?>>> SUBTYPES_BY_PRIMITIVE;
  private static final int VALIDATOR_TYPE_INDEX = 1;
  private static final Log log = LoggerFactory.make();

  static {
    Map<Class<?>, Set<Class<?>>> subtypesByPrimitive = newHashMap();

    putPrimitiveSubtypes(subtypesByPrimitive, Void.TYPE);
    putPrimitiveSubtypes(subtypesByPrimitive, Boolean.TYPE);
    putPrimitiveSubtypes(subtypesByPrimitive, Byte.TYPE);
    putPrimitiveSubtypes(subtypesByPrimitive, Character.TYPE);
    putPrimitiveSubtypes(subtypesByPrimitive, Short.TYPE, Byte.TYPE);
    putPrimitiveSubtypes(subtypesByPrimitive, Integer.TYPE, Character.TYPE, Short.TYPE);
    putPrimitiveSubtypes(subtypesByPrimitive, Long.TYPE, Integer.TYPE);
    putPrimitiveSubtypes(subtypesByPrimitive, Float.TYPE, Long.TYPE);
    putPrimitiveSubtypes(subtypesByPrimitive, Double.TYPE, Float.TYPE);

    SUBTYPES_BY_PRIMITIVE = Collections.unmodifiableMap(subtypesByPrimitive);
  }

  private TypeHelper() {
    throw new AssertionError();
  }

  public static boolean isAssignable(Type supertype, Type type) {
    Contracts.assertNotNull(supertype, "supertype");
    Contracts.assertNotNull(type, "type");

    if (supertype.equals(type)) {
      return true;
    }

    if (supertype instanceof Class<?>) {
      if (type instanceof Class<?>) {
        return isClassAssignable((Class<?>) supertype, (Class<?>) type);
      }

      if (type instanceof ParameterizedType) {
        return isAssignable(supertype, ((ParameterizedType) type).getRawType());
      }

      if (type instanceof TypeVariable<?>) {
        return isTypeVariableAssignable(supertype, (TypeVariable<?>) type);
      }

      if (type instanceof GenericArrayType) {
        if (((Class<?>) supertype).isArray()) {
          return isAssignable(getComponentType(supertype), getComponentType(type));
        }

        return isArraySupertype((Class<?>) supertype);
      }

      if (type instanceof WildcardType) {
        return isClassAssignableToWildcardType((Class<?>) supertype, (WildcardType) type);
      }

      return false;
    }

    if (supertype instanceof ParameterizedType) {
      if (type instanceof Class<?>) {
        return isSuperAssignable(supertype, type);
      }

      if (type instanceof ParameterizedType) {
        return isParameterizedTypeAssignable(
            (ParameterizedType) supertype, (ParameterizedType) type);
      }

      return false;
    }

    if (type instanceof TypeVariable<?>) {
      return isTypeVariableAssignable(supertype, (TypeVariable<?>) type);
    }

    if (supertype instanceof GenericArrayType) {
      if (isArray(type)) {
        return isAssignable(getComponentType(supertype), getComponentType(type));
      }

      return false;
    }

    if (supertype instanceof WildcardType) {
      return isWildcardTypeAssignable((WildcardType) supertype, type);
    }

    return false;
  }

  /**
   * Gets the erased type of the specified type.
   *
   * @param type the type to perform erasure on
   * @return the erased type, never a parameterized type nor a type variable
   * @see <a href="http://java.sun.com/docs/books/jls/third_edition/html/typesValues.html#4.6">4.6
   *     Type Erasure</a>
   */
  public static Type getErasedType(Type type) {
    // the erasure of a parameterized type G<T1, ... ,Tn> is |G|
    if (type instanceof ParameterizedType) {
      Type rawType = ((ParameterizedType) type).getRawType();

      return getErasedType(rawType);
    }

    // TODO: the erasure of a nested type T.C is |T|.C

    // the erasure of an array type T[] is |T|[]
    if (isArray(type)) {
      Type componentType = getComponentType(type);
      Type erasedComponentType = getErasedType(componentType);

      return getArrayType(erasedComponentType);
    }

    // the erasure of a type variable is the erasure of its leftmost bound
    if (type instanceof TypeVariable<?>) {
      Type[] bounds = ((TypeVariable<?>) type).getBounds();

      return getErasedType(bounds[0]);
    }

    // the erasure of every other type is the type itself
    return type;
  }

  private static Class<?> getErasedReferenceType(Type type) {
    Contracts.assertTrue(isReferenceType(type), "type is not a reference type: " + type);
    return (Class<?>) getErasedType(type);
  }

  public static boolean isArray(Type type) {
    return (type instanceof Class<?> && ((Class<?>) type).isArray())
        || (type instanceof GenericArrayType);
  }

  public static Type getComponentType(Type type) {
    if (type instanceof Class<?>) {
      Class<?> klass = (Class<?>) type;

      return klass.isArray() ? klass.getComponentType() : null;
    }

    if (type instanceof GenericArrayType) {
      return ((GenericArrayType) type).getGenericComponentType();
    }

    return null;
  }

  private static Type getArrayType(Type componentType) {
    Contracts.assertNotNull(componentType, "componentType");

    if (componentType instanceof Class<?>) {
      return Array.newInstance((Class<?>) componentType, 0).getClass();
    }

    return genericArrayType(componentType);
  }

  /**
   * Creates a generic array type for the specified component type.
   *
   * @param componentType the component type
   * @return the generic array type
   */
  public static GenericArrayType genericArrayType(final Type componentType) {
    return new GenericArrayType() {

      @Override
      public Type getGenericComponentType() {
        return componentType;
      }
    };
  }

  public static boolean isInstance(Type type, Object object) {
    return getErasedReferenceType(type).isInstance(object);
  }

  /**
   * Creates a parameterized type for the specified raw type and actual type arguments.
   *
   * @param rawType the raw type
   * @param actualTypeArguments the actual type arguments
   * @return the parameterized type
   * @throws MalformedParameterizedTypeException if the number of actual type arguments differs from
   *     those defined on the raw type
   */
  public static ParameterizedType parameterizedType(
      final Class<?> rawType, final Type... actualTypeArguments) {
    return new ParameterizedType() {
      @Override
      public Type[] getActualTypeArguments() {
        return actualTypeArguments;
      }

      @Override
      public Type getRawType() {
        return rawType;
      }

      @Override
      public Type getOwnerType() {
        return null;
      }
    };
  }

  private static Type getResolvedSuperclass(Type type) {
    Contracts.assertNotNull(type, "type");

    Class<?> rawType = getErasedReferenceType(type);
    Type supertype = rawType.getGenericSuperclass();

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

    return resolveTypeVariables(supertype, type);
  }

  private static Type[] getResolvedInterfaces(Type type) {
    Contracts.assertNotNull(type, "type");

    Class<?> rawType = getErasedReferenceType(type);
    Type[] interfaces = rawType.getGenericInterfaces();
    Type[] resolvedInterfaces = new Type[interfaces.length];

    for (int i = 0; i < interfaces.length; i++) {
      resolvedInterfaces[i] = resolveTypeVariables(interfaces[i], type);
    }

    return resolvedInterfaces;
  }

  /**
   * @param annotationType The annotation type.
   * @param validators List of constraint validator classes (for a given constraint).
   * @param <A> the type of the annotation
   * @return Return a Map&lt;Class, Class&lt;? extends ConstraintValidator&gt;&gt; where the map key
   *     is the type the validator accepts and value the validator class itself.
   */
  public static <A extends Annotation>
      Map<Type, Class<? extends ConstraintValidator<A, ?>>> getValidatorsTypes(
          Class<A> annotationType, List<Class<? extends ConstraintValidator<A, ?>>> validators) {
    Map<Type, Class<? extends ConstraintValidator<A, ?>>> validatorsTypes = newHashMap();
    for (Class<? extends ConstraintValidator<A, ?>> validator : validators) {
      Type type = extractType(validator);

      if (validatorsTypes.containsKey(type)) {
        throw log.getMultipleValidatorsForSameTypeException(
            annotationType.getName(), type.toString());
      }

      validatorsTypes.put(type, validator);
    }
    return validatorsTypes;
  }

  private static Type extractType(Class<? extends ConstraintValidator<?, ?>> validator) {
    Map<Type, Type> resolvedTypes = newHashMap();
    Type constraintValidatorType = resolveTypes(resolvedTypes, validator);

    // we now have all bind from a type to its resolution at one level
    Type validatorType =
        ((ParameterizedType) constraintValidatorType)
            .getActualTypeArguments()[VALIDATOR_TYPE_INDEX];
    if (validatorType == null) {
      throw log.getNullIsAnInvalidTypeForAConstraintValidatorException();
    } else if (validatorType instanceof GenericArrayType) {
      validatorType = TypeHelper.getArrayType(TypeHelper.getComponentType(validatorType));
    }

    while (resolvedTypes.containsKey(validatorType)) {
      validatorType = resolvedTypes.get(validatorType);
    }
    // FIXME raise an exception if validatorType is not a class
    return validatorType;
  }

  private static Type resolveTypes(Map<Type, Type> resolvedTypes, Type type) {
    if (type == null) {
      return null;
    } else if (type instanceof Class) {
      Class<?> clazz = (Class<?>) type;
      final Type returnedType = resolveTypeForClassAndHierarchy(resolvedTypes, clazz);
      if (returnedType != null) {
        return returnedType;
      }
    } else if (type instanceof ParameterizedType) {
      ParameterizedType paramType = (ParameterizedType) type;
      if (!(paramType.getRawType() instanceof Class)) {
        return null; // don't know what to do here
      }
      Class<?> rawType = (Class<?>) paramType.getRawType();

      TypeVariable<?>[] originalTypes = rawType.getTypeParameters();
      Type[] partiallyResolvedTypes = paramType.getActualTypeArguments();
      int nbrOfParams = originalTypes.length;
      for (int i = 0; i < nbrOfParams; i++) {
        resolvedTypes.put(originalTypes[i], partiallyResolvedTypes[i]);
      }

      if (rawType.equals(ConstraintValidator.class)) {
        // we found our baby
        return type;
      } else {
        Type returnedType = resolveTypeForClassAndHierarchy(resolvedTypes, rawType);
        if (returnedType != null) {
          return returnedType;
        }
      }
    }
    // else we don't care I think
    return null;
  }

  private static Type resolveTypeForClassAndHierarchy(
      Map<Type, Type> resolvedTypes, Class<?> clazz) {
    Type returnedType = resolveTypes(resolvedTypes, clazz.getGenericSuperclass());
    if (returnedType != null) {
      return returnedType;
    }
    for (Type genericInterface : clazz.getGenericInterfaces()) {
      returnedType = resolveTypes(resolvedTypes, genericInterface);
      if (returnedType != null) {
        return returnedType;
      }
    }
    return null;
  }

  private static void putPrimitiveSubtypes(
      Map<Class<?>, Set<Class<?>>> subtypesByPrimitive,
      Class<?> primitiveType,
      Class<?>... directSubtypes) {
    Set<Class<?>> subtypes = newHashSet();

    for (Class<?> directSubtype : directSubtypes) {
      subtypes.add(directSubtype);
      subtypes.addAll(subtypesByPrimitive.get(directSubtype));
    }

    subtypesByPrimitive.put(primitiveType, Collections.unmodifiableSet(subtypes));
  }

  private static boolean isClassAssignable(Class<?> supertype, Class<?> type) {
    // Class.isAssignableFrom does not perform primitive widening
    if (supertype.isPrimitive() && type.isPrimitive()) {
      return SUBTYPES_BY_PRIMITIVE.get(supertype).contains(type);
    }

    return supertype.isAssignableFrom(type);
  }

  private static boolean isClassAssignableToWildcardType(Class<?> supertype, WildcardType type) {
    for (Type upperBound : type.getUpperBounds()) {
      if (!isAssignable(supertype, upperBound)) {
        return false;
      }
    }

    return true;
  }

  private static boolean isParameterizedTypeAssignable(
      ParameterizedType supertype, ParameterizedType type) {
    Type rawSupertype = supertype.getRawType();
    Type rawType = type.getRawType();

    if (!rawSupertype.equals(rawType)) {
      // short circuit when class raw types are unassignable
      if (rawSupertype instanceof Class<?>
          && rawType instanceof Class<?>
          && !(((Class<?>) rawSupertype).isAssignableFrom((Class<?>) rawType))) {
        return false;
      }

      return isSuperAssignable(supertype, type);
    }

    Type[] supertypeArgs = supertype.getActualTypeArguments();
    Type[] typeArgs = type.getActualTypeArguments();

    if (supertypeArgs.length != typeArgs.length) {
      return false;
    }

    for (int i = 0; i < supertypeArgs.length; i++) {
      Type supertypeArg = supertypeArgs[i];
      Type typeArg = typeArgs[i];

      if (supertypeArg instanceof WildcardType) {
        if (!isWildcardTypeAssignable((WildcardType) supertypeArg, typeArg)) {
          return false;
        }
      } else if (!supertypeArg.equals(typeArg)) {
        return false;
      }
    }

    return true;
  }

  private static boolean isTypeVariableAssignable(Type supertype, TypeVariable<?> type) {
    for (Type bound : type.getBounds()) {
      if (isAssignable(supertype, bound)) {
        return true;
      }
    }

    return false;
  }

  private static boolean isWildcardTypeAssignable(WildcardType supertype, Type type) {
    for (Type upperBound : supertype.getUpperBounds()) {
      if (!isAssignable(upperBound, type)) {
        return false;
      }
    }

    for (Type lowerBound : supertype.getLowerBounds()) {
      if (!isAssignable(type, lowerBound)) {
        return false;
      }
    }

    return true;
  }

  private static boolean isSuperAssignable(Type supertype, Type type) {
    Type superclass = getResolvedSuperclass(type);

    if (superclass != null && isAssignable(supertype, superclass)) {
      return true;
    }

    for (Type interphace : getResolvedInterfaces(type)) {
      if (isAssignable(supertype, interphace)) {
        return true;
      }
    }

    return false;
  }

  /**
   * Gets whether the specified type is a <em>reference type</em>.
   *
   * <p>More specifically, this method returns {@code true} if the specified type is one of the
   * following:
   *
   * <ul>
   *   <li>a class type
   *   <li>an interface type
   *   <li>an array type
   *   <li>a parameterized type
   *   <li>a type variable
   *   <li>the null type
   * </ul>
   *
   * @param type the type to check
   * @return {@code true} if the specified type is a reference type
   * @see <a href="http://java.sun.com/docs/books/jls/third_edition/html/typesValues.html#4.3">4.3
   *     Reference Types and Values</a>
   */
  private static boolean isReferenceType(Type type) {
    return type == null
        || type instanceof Class<?>
        || type instanceof ParameterizedType
        || type instanceof TypeVariable<?>
        || type instanceof GenericArrayType;
  }

  private static boolean isArraySupertype(Class<?> type) {
    return Object.class.equals(type)
        || Cloneable.class.equals(type)
        || Serializable.class.equals(type);
  }

  private static Type resolveTypeVariables(Type type, Type subtype) {
    // TODO: need to support other types in future, e.g. T[], etc.
    if (!(type instanceof ParameterizedType)) {
      return type;
    }

    Map<Type, Type> actualTypeArgumentsByParameter =
        getActualTypeArgumentsByParameter(type, subtype);
    Class<?> rawType = getErasedReferenceType(type);

    return parameterizeClass(rawType, actualTypeArgumentsByParameter);
  }

  private static Map<Type, Type> getActualTypeArgumentsByParameter(Type... types) {
    // TODO: return Map<TypeVariable<Class<?>>, Type> somehow

    Map<Type, Type> actualTypeArgumentsByParameter = new LinkedHashMap<Type, Type>();

    for (Type type : types) {
      actualTypeArgumentsByParameter.putAll(getActualTypeArgumentsByParameterInternal(type));
    }

    return normalize(actualTypeArgumentsByParameter);
  }

  private static Map<Type, Type> getActualTypeArgumentsByParameterInternal(Type type) {
    // TODO: look deeply within non-parameterized types when visitors implemented
    if (!(type instanceof ParameterizedType)) {
      return Collections.emptyMap();
    }

    TypeVariable<?>[] typeParameters = getErasedReferenceType(type).getTypeParameters();
    Type[] typeArguments = ((ParameterizedType) type).getActualTypeArguments();

    if (typeParameters.length != typeArguments.length) {
      throw new MalformedParameterizedTypeException();
    }

    Map<Type, Type> actualTypeArgumentsByParameter = new LinkedHashMap<Type, Type>();

    for (int i = 0; i < typeParameters.length; i++) {
      actualTypeArgumentsByParameter.put(typeParameters[i], typeArguments[i]);
    }

    return actualTypeArgumentsByParameter;
  }

  private static ParameterizedType parameterizeClass(
      Class<?> type, Map<Type, Type> actualTypeArgumentsByParameter) {
    return parameterizeClassCapture(type, actualTypeArgumentsByParameter);
  }

  private static <T> ParameterizedType parameterizeClassCapture(
      Class<T> type, Map<Type, Type> actualTypeArgumentsByParameter) {
    // TODO: actualTypeArgumentsByParameter should be Map<TypeVariable<Class<T>>, Type>

    TypeVariable<Class<T>>[] typeParameters = type.getTypeParameters();
    Type[] actualTypeArguments = new Type[typeParameters.length];

    for (int i = 0; i < typeParameters.length; i++) {
      TypeVariable<Class<T>> typeParameter = typeParameters[i];
      Type actualTypeArgument = actualTypeArgumentsByParameter.get(typeParameter);

      if (actualTypeArgument == null) {
        throw log.getMissingActualTypeArgumentForTypeParameterException(typeParameter);
      }

      actualTypeArguments[i] = actualTypeArgument;
    }

    return parameterizedType(getErasedReferenceType(type), actualTypeArguments);
  }

  private static <K, V> Map<K, V> normalize(Map<K, V> map) {
    // TODO: will this cause an infinite loop with recursive bounds?

    for (Entry<K, V> entry : map.entrySet()) {
      K key = entry.getKey();
      V value = entry.getValue();

      while (map.containsKey(value)) {
        value = map.get(value);
      }

      map.put(key, value);
    }

    return map;
  }
}