Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HV-1552 Adding new MinAge Constraint #913

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.hibernate.validator.cfg.defs;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs a license header comment as in the other classes

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @marko-bekhta I have done all your suggestions except the one for ChronoUnit attribute. I'm not sure what you mean. Is it something like this: add to @interface AgeMin a attribute like ChronoUnit unit();, so users can define the value when use the annotation like this:
@AgeMin( value = MINIMUM_AGE , inclusive = true, unit= ChronoUnit.YEARS)
or @AgeMin( value = MINIMUM_AGE , inclusive = true, unit= ChronoUnit.MONTHS) ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @Hilmerc yes, that's exactly it! :) This will make the constraint more versatile.
I've also prepared a short plan, with items that are still needed to finish this work. I'll post it in the separate comment. I hope it'll be helpful.


import org.hibernate.validator.cfg.ConstraintDef;
import org.hibernate.validator.constraints.AgeMin;

public class AgeMinDef extends ConstraintDef<AgeMinDef, AgeMin> {

public AgeMinDef() {
super( AgeMin.class );
}

public AgeMinDef value(int value) {
addParameter( "value", value );
return this;
}

public AgeMinDef inclusive(boolean inclusive) {
addParameter( "inclusive", inclusive );
return this;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Hibernate Validator, declare and validate application constraints
*
* License: Apache License, Version 2.0
* See the license.txt file in the root directory or <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package org.hibernate.validator.constraints;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;


import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
* The annotated element must be a date where the number of years go by to today must be
* greater or equal to the specified value
* <p>
* <p>
* The supported type is {@code LocalDate}. {@code null} is considered valid.
* <p>
*
* @author Hillmer Chona
* @since 6.0.8
*/
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Repeatable(AgeMin.List.class)
@Documented
@Constraint(validatedBy = {})
public @interface AgeMin {

String message() default "{org.hibernate.validator.constraints.AgeMin.message}";

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};

/**
* @return value the age in years from a given date must be greater or equal to
*/
int value();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in the previous discussion, there was an idea to try out to support other units (ChronoUnit) with the ChronoUnit#YEARS by default. Could you please try to experiment with that ? Should be simple to add something like ChronoUnit unit() default ChronoUnit.YEARS; to the constraint. If it works the programmatic definition (AgeMinDef) would need this attribute to be added as well.


/**
* Specifies whether the specified value is inclusive or exclusive.
* By default, it is inclusive.
*
* @return {@code true} if the number of years from a given date must be higher or equal to the specified value,
* {@code false} if the number of years from a given date must be higher
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

years -> date period units ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, Done

*/
boolean inclusive() default true;

/**
* Defines several {@link AgeMin} annotations on the same element.
*
* @see AgeMin
*/
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Documented
@interface List {
AgeMin[] value();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Hibernate Validator, declare and validate application constraints
*
* License: Apache License, Version 2.0
* See the license.txt file in the root directory or <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package org.hibernate.validator.internal.constraintvalidators.hv;

import org.hibernate.validator.constraints.AgeMin;
import org.hibernate.validator.constraintvalidation.HibernateConstraintValidator;
import org.hibernate.validator.constraintvalidation.HibernateConstraintValidatorInitializationContext;
import org.hibernate.validator.internal.util.logging.Log;
import org.hibernate.validator.internal.util.logging.LoggerFactory;

import javax.validation.ConstraintValidatorContext;
import javax.validation.metadata.ConstraintDescriptor;

import java.lang.invoke.MethodHandles;
import java.time.Clock;
import java.time.Duration;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;

/**
*
* Checks that the number of years from a given date to today is greater or equal to
* a specified value
*
* @author Hillmer Chona
*/
public class AgeMinValidator implements HibernateConstraintValidator<AgeMin, LocalDate> {

private static final Log LOG = LoggerFactory.make( MethodHandles.lookup() );

private int minAge;

private boolean inclusive;

protected Clock referenceClock;

@Override
public void initialize(ConstraintDescriptor<AgeMin> constraintDescriptor, HibernateConstraintValidatorInitializationContext initializationContext) {
try {
this.referenceClock = Clock.offset(
initializationContext.getClockProvider().getClock(),
getEffectiveTemporalValidationTolerance( initializationContext.getTemporalValidationTolerance() )
);
this.minAge = constraintDescriptor.getAnnotation().value();
this.inclusive = constraintDescriptor.getAnnotation().inclusive();

}
catch (Exception e) {
throw LOG.getUnableToGetCurrentTimeFromClockProvider( e );
}
}

@Override
public boolean isValid(LocalDate date, ConstraintValidatorContext constraintValidatorContext) {
// null values are valid
if ( date == null ) {
return true;
}
return inclusive ? ChronoUnit.YEARS.between( date, getReferenceValue( referenceClock ) ) >= minAge : ChronoUnit.YEARS.between( date, getReferenceValue( referenceClock ) ) > minAge;
}

private LocalDate getReferenceValue(Clock reference) {
return LocalDate.now( reference );
}

protected Duration getEffectiveTemporalValidationTolerance(Duration absoluteTemporalValidationTolerance) {
return absoluteTemporalValidationTolerance.negated();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
import org.hibernate.validator.constraints.ISBN;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.LuhnCheck;
import org.hibernate.validator.constraints.AgeMin;
import org.hibernate.validator.constraints.Mod10Check;
import org.hibernate.validator.constraints.Mod11Check;
import org.hibernate.validator.constraints.ModCheck;
Expand Down Expand Up @@ -258,6 +259,7 @@
import org.hibernate.validator.internal.constraintvalidators.hv.ISBNValidator;
import org.hibernate.validator.internal.constraintvalidators.hv.LengthValidator;
import org.hibernate.validator.internal.constraintvalidators.hv.LuhnCheckValidator;
import org.hibernate.validator.internal.constraintvalidators.hv.AgeMinValidator;
import org.hibernate.validator.internal.constraintvalidators.hv.Mod10CheckValidator;
import org.hibernate.validator.internal.constraintvalidators.hv.Mod11CheckValidator;
import org.hibernate.validator.internal.constraintvalidators.hv.ModCheckValidator;
Expand Down Expand Up @@ -462,6 +464,8 @@ public ConstraintHelper() {
) );
}

putConstraint( tmpConstraints, AgeMin.class, AgeMinValidator.class );

if ( isJavaMoneyInClasspath() ) {
putConstraints( tmpConstraints, Negative.class, Arrays.asList(
NegativeValidatorForBigDecimal.class,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ org.hibernate.validator.constraints.ISBN.message = invalid IS
org.hibernate.validator.constraints.Length.message = length must be between {min} and {max}
org.hibernate.validator.constraints.CodePointLength.message = length must be between {min} and {max}
org.hibernate.validator.constraints.LuhnCheck.message = The check digit for ${validatedValue} is invalid, Luhn Modulo 10 checksum failed
org.hibernate.validator.constraints.AgeMin.message = must be older than {value}
org.hibernate.validator.constraints.Mod10Check.message = The check digit for ${validatedValue} is invalid, Modulo 10 checksum failed
org.hibernate.validator.constraints.Mod11Check.message = The check digit for ${validatedValue} is invalid, Modulo 11 checksum failed
org.hibernate.validator.constraints.ModCheck.message = The check digit for ${validatedValue} is invalid, ${modType} checksum failed
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* Hibernate Validator, declare and validate application constraints
*
* License: Apache License, Version 2.0
* See the license.txt file in the root directory or <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package org.hibernate.validator.test.internal.constraintvalidators.hv;

import static java.lang.annotation.ElementType.FIELD;
import static org.hibernate.validator.testutil.ConstraintViolationAssert.assertNoViolations;
import static org.hibernate.validator.testutil.ConstraintViolationAssert.assertThat;
import static org.hibernate.validator.testutil.ConstraintViolationAssert.violationOf;
import static org.hibernate.validator.testutils.ValidatorUtil.getConfiguration;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;

import java.util.Set;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;

import org.hibernate.validator.HibernateValidator;
import org.hibernate.validator.HibernateValidatorConfiguration;
import org.hibernate.validator.cfg.ConstraintMapping;
import org.hibernate.validator.cfg.defs.AgeMinDef;
import org.hibernate.validator.constraints.AgeMin;
import org.hibernate.validator.constraintvalidation.HibernateConstraintValidator;
import org.hibernate.validator.internal.constraintvalidators.hv.AgeMinValidator;
import org.hibernate.validator.internal.util.annotation.ConstraintAnnotationDescriptor;
import org.hibernate.validator.testutil.TestForIssue;

import org.hibernate.validator.testutils.ConstraintValidatorInitializationHelper;
import org.testng.annotations.Test;

import java.time.LocalDate;

/**
* A set of tests for {@link AgeMin} constraint validator ({@link AgeMinValidator}), which
* make sure that validation is performed correctly.
*
* @author Hillmer Chona
* @since 6.0.8
*/
@TestForIssue(jiraKey = "HV-1552" )
public class AgeValidatorTest {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These set of tests would check if AgeMinValidator works as expected as well as the programmatic definition. Could you also add another test class similar for example to this one - https://github.com/hibernate/hibernate-validator/blob/master/engine/src/test/java/org/hibernate/validator/test/constraints/annotations/hv/ISBNConstrainedTest.java
We add such tests in that package to make sure that the constraint and corresponding validators are correctly registered and also that the default message is present.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


private int value = 18;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe make a constant of it and use in other occurrences of this number as well. WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


/**
* @return an initialized {@link ConstraintValidator} using {@code DUMMY_CONSTRAINT_VALIDATOR_INITIALIZATION_CONTEXT}
*/
private ConstraintValidator<AgeMin, LocalDate> getInitializedValidator(int value, boolean inclusive) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually we place private methods at the end of the test class and start with the tests. It's easier to see what is tested this way.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

HibernateConstraintValidator<AgeMin, LocalDate> validator = new AgeMinValidator();
ConstraintAnnotationDescriptor.Builder<AgeMin> descriptorBuilder = new ConstraintAnnotationDescriptor.Builder<>( AgeMin.class );
descriptorBuilder.setAttribute( "value", value );
descriptorBuilder.setAttribute( "inclusive", inclusive );
ConstraintAnnotationDescriptor<AgeMin> descriptor = descriptorBuilder.build();
ConstraintValidatorInitializationHelper.initialize( validator, descriptor );
return validator;
}

private void assertValidAge(LocalDate birthDate, ConstraintValidator<AgeMin, LocalDate> constraintValidator) {
assertTrue( constraintValidator.isValid( birthDate, null ), birthDate + " should be a date equal or more than " + value + " years before today" );
}

private void assertInvalidAge(LocalDate birthDate, ConstraintValidator<AgeMin, LocalDate> constraintValidator) {
assertFalse( constraintValidator.isValid( birthDate, null ), birthDate + " should be a date less than " + value + " years before today" );
}

@Test
public void testLocalDate() throws Exception {
ConstraintValidator<AgeMin, LocalDate> constraintValidator = getInitializedValidator( value, true );

LocalDate todayMinus18Years = LocalDate.now().minusYears( 18 );
LocalDate todayMinus2MonthAnd18Years = LocalDate.now().minusMonths( 2 ).minusYears( 18 );
LocalDate tomorrowMinus18Years = LocalDate.now().plusDays( 1 ).minusYears( 18 );

assertValidAge( null , constraintValidator );
assertValidAge( todayMinus18Years, constraintValidator );
assertValidAge( todayMinus2MonthAnd18Years, constraintValidator );
assertInvalidAge( tomorrowMinus18Years, constraintValidator );
}

@Test
public void testInclusiveLocalDate() throws Exception {
ConstraintValidator<AgeMin, LocalDate> constraintValidatorInclusiveTrue = getInitializedValidator( value, true );
ConstraintValidator<AgeMin, LocalDate> constraintValidatorInclusiveFalse = getInitializedValidator( value, false );

LocalDate todayMinus18Years = LocalDate.now().minusYears( 18 );

assertValidAge( todayMinus18Years, constraintValidatorInclusiveTrue );
assertInvalidAge( todayMinus18Years, constraintValidatorInclusiveFalse );
}

@Test
public void testProgrammaticDefinition() throws Exception {
HibernateValidatorConfiguration config = getConfiguration( HibernateValidator.class );
ConstraintMapping mapping = config.createConstraintMapping();
mapping.type( User.class )
.property( "birthDate" , FIELD )
.constraint( new AgeMinDef().value( this.value ) );
config.addMapping( mapping );
Validator validator = config.buildValidatorFactory().getValidator();

LocalDate todayMinus18Years = LocalDate.now().minusYears( 18 );
LocalDate tomorrowMinus18Years = LocalDate.now().plusDays( 1 ).minusYears( 18 );

Set<ConstraintViolation<User>> constraintViolations = validator.validate( new User( todayMinus18Years ) );
assertNoViolations( constraintViolations );

constraintViolations = validator.validate( new User( tomorrowMinus18Years ) );
assertThat( constraintViolations ).containsOnlyViolations(
violationOf( AgeMin.class )
);
}

private static class User {
private final LocalDate birthDate;

public User(LocalDate birthDate) {
this.birthDate = birthDate;
}
}
}