protected ConstraintValidationResult processSingleLengthConstraint(
      DictionaryValidationResult result,
      Object value,
      LengthConstraint constraint,
      AttributeValueReader attributeValueReader)
      throws AttributeValidationException {
    // Can't process any range constraints on null values
    if (ValidationUtils.isNullOrEmpty(value)) {
      return result.addSkipped(attributeValueReader, CONSTRAINT_NAME);
    }

    DataType dataType = constraint.getDataType();
    Object typedValue = value;

    if (dataType != null) {
      typedValue = ValidationUtils.convertToDataType(value, dataType, dateTimeService);
    }

    // The only thing that can have a length constraint currently is a string.
    if (typedValue instanceof String) {
      return validateLength(result, (String) typedValue, constraint, attributeValueReader);
    }

    return result.addSkipped(attributeValueReader, CONSTRAINT_NAME);
  }
  /**
   * validates the date value using the range constraint provided
   *
   * @param result - a holder for any already run validation results
   * @param value - the value to validate
   * @param constraint - the range constraint to use
   * @param attributeValueReader - provides access to the attribute being validated
   * @return the passed in result, updated with the results of the processing
   * @throws IllegalArgumentException
   */
  protected ConstraintValidationResult validateRange(
      DictionaryValidationResult result,
      Date value,
      RangeConstraint constraint,
      AttributeValueReader attributeValueReader)
      throws IllegalArgumentException {

    Date date = value != null ? ValidationUtils.getDate(value, dateTimeService) : null;

    String inclusiveMaxText = constraint.getInclusiveMax();
    String exclusiveMinText = constraint.getExclusiveMin();

    Date inclusiveMax =
        inclusiveMaxText != null
            ? ValidationUtils.getDate(inclusiveMaxText, dateTimeService)
            : null;
    Date exclusiveMin =
        exclusiveMinText != null
            ? ValidationUtils.getDate(exclusiveMinText, dateTimeService)
            : null;

    return isInRange(
        result,
        date,
        inclusiveMax,
        inclusiveMaxText,
        exclusiveMin,
        exclusiveMinText,
        attributeValueReader);
  }
  protected ConstraintValidationResult validateLength(
      DictionaryValidationResult result,
      String value,
      LengthConstraint constraint,
      AttributeValueReader attributeValueReader)
      throws IllegalArgumentException {
    Integer valueLength = Integer.valueOf(value.length());

    Integer maxLength = constraint.getMaxLength();
    Integer minLength = constraint.getMinLength();

    Result lessThanMax = ValidationUtils.isLessThanOrEqual(valueLength, maxLength);
    Result greaterThanMin = ValidationUtils.isGreaterThanOrEqual(valueLength, minLength);

    // It's okay for one end of the range to be undefined - that's not an error. It's only an error
    // if one of them is invalid
    if (lessThanMax != Result.INVALID && greaterThanMin != Result.INVALID) {
      // Of course, if they're both undefined then we didn't actually have a real constraint
      if (lessThanMax == Result.UNDEFINED && greaterThanMin == Result.UNDEFINED) {
        return result.addNoConstraint(attributeValueReader, CONSTRAINT_NAME);
      }

      // In this case, we've succeeded
      return result.addSuccess(attributeValueReader, CONSTRAINT_NAME);
    }

    String maxErrorParameter = maxLength != null ? maxLength.toString() : null;
    String minErrorParameter = minLength != null ? minLength.toString() : null;

    // If both comparisons happened then if either comparison failed we can show the end user the
    // expected range on both sides.
    if (lessThanMax != Result.UNDEFINED && greaterThanMin != Result.UNDEFINED) {
      return result.addError(
          RANGE_KEY,
          attributeValueReader,
          CONSTRAINT_NAME,
          RiceKeyConstants.ERROR_OUT_OF_RANGE,
          minErrorParameter,
          maxErrorParameter);
    }
    // If it's the max comparison that fails, then just tell the end user what the max can be
    else if (lessThanMax == Result.INVALID) {
      return result.addError(
          MAX_LENGTH_KEY,
          attributeValueReader,
          CONSTRAINT_NAME,
          RiceKeyConstants.ERROR_INCLUSIVE_MAX,
          maxErrorParameter);
    }
    // Otherwise, just tell them what the min can be
    else {
      return result.addError(
          MIN_LENGTH_KEY,
          attributeValueReader,
          CONSTRAINT_NAME,
          RiceKeyConstants.ERROR_EXCLUSIVE_MIN,
          minErrorParameter);
    }
  }
  /**
   * checks whether the value provided is in the range specified by inclusiveMax and exclusiveMin
   *
   * @param result a holder for any already run validation results
   * @param value the value to check
   * @param inclusiveMax the maximum value of the attribute
   * @param inclusiveMaxText the string representation of inclusiveMax
   * @param exclusiveMin the minimum value of the attribute
   * @param exclusiveMinText the string representation of exclusiveMin
   * @param attributeValueReader provides access to the attribute being validated
   * @return the passed in result, updated with the results of the range check
   */
  private <T> ConstraintValidationResult isInRange(
      DictionaryValidationResult result,
      T value,
      Comparable<T> inclusiveMax,
      String inclusiveMaxText,
      Comparable<T> exclusiveMin,
      String exclusiveMinText,
      AttributeValueReader attributeValueReader) {
    // What we want to know is that the maximum value is greater than or equal to the number passed
    // (the number can be equal to the max, i.e. it's 'inclusive')
    Result lessThanMax = ValidationUtils.isLessThanOrEqual(value, inclusiveMax);
    // On the other hand, since the minimum is exclusive, we just want to make sure it's less than
    // the number (the number can't be equal to the min, i.e. it's 'exclusive')
    Result greaterThanMin = ValidationUtils.isGreaterThan(value, exclusiveMin);

    // It's okay for one end of the range to be undefined - that's not an error. It's only an error
    // if one of them is actually invalid.
    if (lessThanMax != Result.INVALID && greaterThanMin != Result.INVALID) {
      // Of course, if they're both undefined then we didn't actually have a real constraint
      if (lessThanMax == Result.UNDEFINED && greaterThanMin == Result.UNDEFINED) {
        return result.addNoConstraint(attributeValueReader, CONSTRAINT_NAME);
      }

      // In this case, we've succeeded
      return result.addSuccess(attributeValueReader, CONSTRAINT_NAME);
    }

    // If both comparisons happened then if either comparison failed we can show the end user the
    // expected range on both sides.
    if (lessThanMax != Result.UNDEFINED && greaterThanMin != Result.UNDEFINED) {
      return result.addError(
          RANGE_KEY,
          attributeValueReader,
          CONSTRAINT_NAME,
          RiceKeyConstants.ERROR_OUT_OF_RANGE,
          exclusiveMinText,
          inclusiveMaxText);
    }
    // If it's the max comparison that fails, then just tell the end user what the max can be
    else if (lessThanMax == Result.INVALID) {
      return result.addError(
          MAX_INCLUSIVE_KEY,
          attributeValueReader,
          CONSTRAINT_NAME,
          RiceKeyConstants.ERROR_INCLUSIVE_MAX,
          inclusiveMaxText);
    }
    // Otherwise, just tell them what the min can be
    else {
      return result.addError(
          MIN_EXCLUSIVE_KEY,
          attributeValueReader,
          CONSTRAINT_NAME,
          RiceKeyConstants.ERROR_EXCLUSIVE_MIN,
          exclusiveMinText);
    }
  }
  /**
   * validates the value provided using {@code RangeConstraint}
   *
   * @param result - a holder for any already run validation results
   * @param value - the value to validate
   * @param constraint - the range constraint to use
   * @param attributeValueReader - provides access to the attribute being validated
   * @return the passed in result, updated with the results of the processing
   * @throws AttributeValidationException if validation fails
   */
  protected ConstraintValidationResult processSingleRangeConstraint(
      DictionaryValidationResult result,
      Object value,
      RangeConstraint constraint,
      AttributeValueReader attributeValueReader)
      throws AttributeValidationException {
    // Can't process any range constraints on null values
    if (ValidationUtils.isNullOrEmpty(value)
        || (constraint.getExclusiveMin() == null && constraint.getInclusiveMax() == null)) {
      return result.addSkipped(attributeValueReader, CONSTRAINT_NAME);
    }

    // This is necessary because sometimes we'll be getting a string, for example, that represents a
    // date.
    DataType dataType = constraint.getDataType();
    Object typedValue = value;

    if (dataType != null) {
      typedValue = ValidationUtils.convertToDataType(value, dataType, dateTimeService);
    } else if (value instanceof String) {
      // assume string is a number of type double
      try {
        Double d = Double.parseDouble((String) value);
        typedValue = d;
      } catch (NumberFormatException n) {
        // do nothing, typedValue is never reset
      }
    }

    // TODO: decide if there is any reason why the following would be insufficient - i.e. if
    // something numeric could still be cast to String at this point
    if (typedValue instanceof Date) {
      return validateRange(result, (Date) typedValue, constraint, attributeValueReader);
    } else if (typedValue instanceof Number) {
      return validateRange(result, (Number) typedValue, constraint, attributeValueReader);
    }

    return result.addSkipped(attributeValueReader, CONSTRAINT_NAME);
  }