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
Changes from all 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,36 @@
/*
* 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.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;

/**
* @author Hillmer Chona
* @since 6.0.8
*/
public class AgeMinDef extends ConstraintDef<AgeMinDef, AgeMin> {

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

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

public AgeMinDef unit(AgeMin.Unit unit) {
addParameter( "unit", unit );
return this;
}

public AgeMinDef inclusive(boolean inclusive) {
addParameter( "inclusive", inclusive );
return this;
}
}
115 changes: 115 additions & 0 deletions engine/src/main/java/org/hibernate/validator/constraints/AgeMin.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* 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 java.time.temporal.ChronoUnit;


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 an instant, date or time for which at least
* the specified amount ({@link AgeMin#value()}) of Years/Days/Months/etc. defined
* by {@link AgeMin#unit()} have passed till now.
* <p>
* Supported types are:
* <ul>
* <li>{@code java.util.Calendar}</li>
* <li>{@code java.util.Date}</li>
* <li>{@code java.time.chrono.HijrahDate}</li>
* <li>{@code java.time.chrono.JapaneseDate}</li>
* <li>{@code java.time.LocalDate}</li>
* <li>{@code java.time.chrono.MinguoDate}</li>
* <li>{@code java.time.chrono.ThaiBuddhistDate}</li>
* <li>{@code java.time.Year}</li>
* <li>{@code java.time.YearMonth}</li>
* </ul>
* <p>
* {@code null} elements are considered valid.
*
* @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 the age according to unit from a given instant, date or time 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 the date period unit ( Years/Days/Months. ) that will be used to compare the given instant,
* date or time with the reference value.
* By default, it is ({@link AgeMin.Unit#YEARS}).
*
* @return the date period unit
*/

Unit unit() default Unit.YEARS;

/**
* Specifies whether the specified value is inclusive or exclusive.
* By default, it is inclusive.
*
* @return {@code true} if the date period units from a given instant, date or time must be higher or equal to the specified value,
* {@code false} if date period units from a given instant, date or time must be higher
*/
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();
}

enum Unit {
YEARS( ChronoUnit.YEARS ), MONTHS( ChronoUnit.MONTHS ), DAYS( ChronoUnit.DAYS );

private final ChronoUnit chronoUnit;

Unit(ChronoUnit chronoUnit) {
this.chronoUnit = chronoUnit;

}

public ChronoUnit getChronoUnit() {
return chronoUnit;
}

}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* 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.age;

import java.lang.annotation.Annotation;
import java.lang.invoke.MethodHandles;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;

import javax.validation.ConstraintValidatorContext;

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;

/**
* Base class for all age validators that use an {@link Instant} to be compared to the age reference.
*
* @author Hillmer Chona
* @since 6.0.8
*/
public abstract class AbstractAgeInstantBasedValidator<C extends Annotation, T>
implements HibernateConstraintValidator<C, T> {

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

private Clock referenceClock;

private int referenceAge;

private boolean inclusive;

private ChronoUnit unit;

public void initialize(
int referenceAge,
ChronoUnit unit,
boolean inclusive,
HibernateConstraintValidatorInitializationContext initializationContext) {
try {
this.referenceClock = Clock.offset(
initializationContext.getClockProvider().getClock(),
getEffectiveTemporalValidationTolerance( initializationContext.getTemporalValidationTolerance() )
);
}
catch (Exception e) {
throw LOG.getUnableToGetCurrentTimeFromClockProvider( e );
}
this.referenceAge = referenceAge;
this.unit = unit;
this.inclusive = inclusive;
}

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 we can add

public void initialize(int referenceAge, ChronoUnit unit, boolean inclusive, HibernateConstraintValidatorInitializationContext initializationContext) {
    try {
        this.referenceClock = Clock.offset(
                initializationContext.getClockProvider().getClock(),
                getEffectiveTemporalValidationTolerance( initializationContext.getTemporalValidationTolerance() )
        );
    }
    catch (Exception e) {
        throw LOG.getUnableToGetCurrentTimeFromClockProvider( e );
    }
    this.referenceAge = referenceAge;
    this.unit = unit;
    this.inclusive = inclusive;
}

here. This way we will not need to repeat same logic for referenceClock in all other validators.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I tried to do something like you say, but no good ideas came to me, thanks. Done.

@Override
public boolean isValid(T value, ConstraintValidatorContext context) {
// null values are valid
if ( value == null ) {
return true;
}
// As Instant does not support plus operation on ChronoUnits greater than DAYS we need to convert it to LocalDate
// first, which supports such operations.

int result = getInstant( value ).atZone( ZoneOffset.ofHours( 0 ) ).toLocalDate()
.compareTo( LocalDate.now( referenceClock ).minus( referenceAge, unit ) );

return isValid( result );
}

/**
* Returns whether the specified value is inclusive or exclusive.
*/
protected boolean isInclusive() {
return this.inclusive;
}

/**
* Returns the temporal validation tolerance to apply.
*/
protected abstract Duration getEffectiveTemporalValidationTolerance(Duration absoluteTemporalValidationTolerance);

/**
* Returns the {@link Instant} measured from Epoch.
*/
protected abstract Instant getInstant(T value);

/**
* Returns whether the result of the comparison between the validated value and the reference age is considered
* valid.
*/
protected abstract boolean isValid(int result);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* 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.age;

import java.lang.annotation.Annotation;
import java.lang.invoke.MethodHandles;
import java.time.Clock;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAccessor;

import javax.validation.ClockProvider;
import javax.validation.ConstraintValidatorContext;

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;

/**
* Base class for all age validators that are based on the {@code java.time} package.
*
* @author Hillmer Chona
* @since 6.0.8
*/
public abstract class AbstractAgeTimeBasedValidator<C extends Annotation, T extends TemporalAccessor & Comparable<? super T>>
implements HibernateConstraintValidator<C, T> {

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

private Clock referenceClock;

private int referenceAge;

private boolean inclusive;

private ChronoUnit unit;

public void initialize(
int referenceAge,
ChronoUnit unit,
boolean inclusive,
HibernateConstraintValidatorInitializationContext initializationContext) {
try {
this.referenceClock = Clock.offset(
initializationContext.getClockProvider().getClock(),
getEffectiveTemporalValidationTolerance( initializationContext.getTemporalValidationTolerance() )
);
}
catch (Exception e) {
throw LOG.getUnableToGetCurrentTimeFromClockProvider( e );
}
this.referenceAge = referenceAge;
this.unit = unit;
this.inclusive = inclusive;
}

@Override
public boolean isValid(T value, ConstraintValidatorContext context) {
// null values are valid
if ( value == null ) {
return true;
}

int result = value.compareTo( getReferenceValue( referenceClock, referenceAge, unit ) );
Copy link
Member

Choose a reason for hiding this comment

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

Maybe better to follow the same pattern as in the instant validatior ? I mean do the operation once here in the validator rather than do the minus in each type's impl. Also I've noticed that you've used the minus for these TemporalAccessor types and for Instant there's add. Would be better to use the same one in both, both are fine just pick one :)


return isValid( result );
}

/**
* Returns whether the specified value is inclusive or exclusive.
*/
protected boolean isInclusive() {
return this.inclusive;
}

/**
* Returns the temporal validation tolerance to apply.
*/
protected abstract Duration getEffectiveTemporalValidationTolerance(Duration absoluteTemporalValidationTolerance);

/**
* Returns an object of the validated type corresponding to the time reference as provided by the
* {@link ClockProvider} increased or decreased with the specified referenceAge of Years/Days/Months/etc.
* defined by {@link ChronoUnit}.
*/
protected abstract T getReferenceValue(Clock reference, int referenceAge, ChronoUnit unit);

/**
* Returns whether the result of the comparison between the validated value and the age reference is considered
* valid.
*/
protected abstract boolean isValid(int result);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* 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.age.min;

import java.time.Duration;
import java.time.Instant;

import javax.validation.metadata.ConstraintDescriptor;

import org.hibernate.validator.constraints.AgeMin;
import org.hibernate.validator.constraintvalidation.HibernateConstraintValidatorInitializationContext;
import org.hibernate.validator.internal.constraintvalidators.hv.age.AbstractAgeInstantBasedValidator;


/**
* Base class for all {@code @AgeMin} validators that use an {@link Instant} to be compared to the age reference.
*
* @author Hillmer Chona
* @since 6.0.8
*/
public abstract class AbstractAgeMinInstantBasedValidator<T> extends AbstractAgeInstantBasedValidator<AgeMin, T> {

@Override
public void initialize(ConstraintDescriptor<AgeMin> constraintDescriptor, HibernateConstraintValidatorInitializationContext initializationContext) {
super.initialize( constraintDescriptor.getAnnotation().value(), constraintDescriptor.getAnnotation().unit().getChronoUnit(),
constraintDescriptor.getAnnotation().inclusive(), initializationContext );
}

@Override
protected Duration getEffectiveTemporalValidationTolerance(Duration absoluteTemporalValidationTolerance) {
return absoluteTemporalValidationTolerance;
}

@Override
protected boolean isValid(int result) {
return isInclusive() ? result <= 0 : result < 0;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* 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.age.min;

import java.time.Duration;
import java.time.temporal.Temporal;
import java.time.temporal.TemporalAccessor;

import javax.validation.metadata.ConstraintDescriptor;

import org.hibernate.validator.constraints.AgeMin;
import org.hibernate.validator.constraintvalidation.HibernateConstraintValidatorInitializationContext;
import org.hibernate.validator.internal.constraintvalidators.hv.age.AbstractAgeTimeBasedValidator;

/**
* Base class for all {@code @AgeMin} validators that are based on the {@code java.time} package.
*
* @author Hillmer Chona
* @since 6.0.8
*/
public abstract class AbstractAgeMinTimeBasedValidator<T extends Temporal & TemporalAccessor & Comparable<? super T>>
extends AbstractAgeTimeBasedValidator<AgeMin, T> {

@Override
public void initialize(
ConstraintDescriptor<AgeMin> constraintDescriptor,
HibernateConstraintValidatorInitializationContext initializationContext) {
super.initialize( constraintDescriptor.getAnnotation().value(),
constraintDescriptor.getAnnotation().unit().getChronoUnit(),
constraintDescriptor.getAnnotation().inclusive(),
initializationContext
);
}

@Override
protected Duration getEffectiveTemporalValidationTolerance(Duration absoluteTemporalValidationTolerance) {
return absoluteTemporalValidationTolerance.negated();
}

@Override
protected boolean isValid(int result) {
return isInclusive() ? result <= 0 : result < 0;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* 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.age.min;
Copy link
Member

Choose a reason for hiding this comment

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

license header is missing

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


import java.time.Instant;

import java.util.Calendar;

/**
* Checks that the number of Years, Days, Months, etc. according to an unit {@code java.time.temporal.ChronoUnit}
* from a given {@code java.util.Calendar} to current day is greater than or equal to the specified value if inclusive is true
* or is greater when inclusive is false.
*
* @author Hillmer Chona
* @since 6.0.8
*/
public class AgeMinValidatorForCalendar extends AbstractAgeMinInstantBasedValidator<Calendar> {

@Override
protected Instant getInstant(Calendar value) {
return value.toInstant();
}



}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* 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.age.min;

import java.time.Instant;

import java.util.Date;

/**
* Checks that the number of Years, Days, Months, etc. according to an unit {@code java.time.temporal.ChronoUnit}
* from a given {@code java.util.Calendar} to current day is greater than or equal to the specified value if inclusive is true
* or is greater when inclusive is false.
*
* @author Hillmer Chona
* @since 6.0.8
*/
public class AgeMinValidatorForDate extends AbstractAgeMinInstantBasedValidator<Date> {

@Override
protected Instant getInstant(Date value) {
return value.toInstant();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* 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.age.min;

import java.time.Clock;
import java.time.chrono.HijrahDate;
import java.time.temporal.ChronoUnit;

/**
* Checks that the number of Years, Days, Months, etc. according to an unit {@code java.time.temporal.ChronoUnit}
* from a given {@code java.time.chrono.HijrahDate} to current day is greater than or equal to the specified value if inclusive is true
* or is greater when inclusive is false.
*
* @author Hillmer Chona
* @since 6.0.8
*/
public class AgeMinValidatorForHijrahDate extends AbstractAgeMinTimeBasedValidator<HijrahDate> {
@Override
protected HijrahDate getReferenceValue(Clock reference, int referenceAge, ChronoUnit unit) {
return HijrahDate.now( reference ).minus( referenceAge, unit );
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* 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.age.min;

import java.time.Clock;
import java.time.chrono.JapaneseDate;
import java.time.temporal.ChronoUnit;

/**
* Checks that the number of Years, Days, Months, etc. according to an unit {@code java.time.temporal.ChronoUnit}
* from a given {@code java.time.chrono.JapaneseDate} to current day is greater than or equal to the specified value if inclusive is true
* or is greater when inclusive is false.
*
* @author Hillmer Chona
* @since 6.0.8
*/
public class AgeMinValidatorForJapaneseDate extends AbstractAgeMinTimeBasedValidator<JapaneseDate> {
@Override
protected JapaneseDate getReferenceValue(
Clock reference, int referenceAge, ChronoUnit unit) {
return JapaneseDate.now( reference ).minus( referenceAge, unit );
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* 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.age.min;
Copy link
Member

Choose a reason for hiding this comment

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

license header is missing

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


import java.time.Clock;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;

/**
* Checks that the number of Years, Days, Months, etc. according to an unit {@code java.time.temporal.ChronoUnit}
* from a given {@code java.time.LocalDate} to current day is greater than or equal to the specified value if inclusive is true
* or is greater when inclusive is false.
*
* @author Hillmer Chona
* @since 6.0.8
*/
public class AgeMinValidatorForLocalDate extends AbstractAgeMinTimeBasedValidator<LocalDate> {

@Override
protected LocalDate getReferenceValue(Clock reference, int referenceAge, ChronoUnit unit) {
return LocalDate.now( reference ).minus( referenceAge, unit );
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* 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.age.min;

import java.time.Clock;
import java.time.chrono.MinguoDate;
import java.time.temporal.ChronoUnit;

/**
* Checks that the number of Years, Days, Months, etc. according to an unit {@code java.time.temporal.ChronoUnit}
* from a given {@code java.time.chrono.MinguoDate} to current day is greater than or equal to the specified value if inclusive is true
* or is greater when inclusive is false.
*
* @author Hillmer Chona
* @since 6.0.8
*/
public class AgeMinValidatorForMinguoDate extends AbstractAgeMinTimeBasedValidator<MinguoDate> {

@Override
protected MinguoDate getReferenceValue(Clock reference, int referenceAge, ChronoUnit unit) {
return MinguoDate.now( reference ).minus( referenceAge, unit );
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* 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.age.min;

import java.time.Clock;
import java.time.chrono.ThaiBuddhistDate;
import java.time.temporal.ChronoUnit;

/**
* Checks that the number of Years, Days, Months, etc. according to an unit {@code java.time.temporal.ChronoUnit}
* from a given {@code java.time.chrono.ThaiBuddhistDate} to current day is greater than or equal to the specified value if inclusive is true
* or is greater when inclusive is false.
*
* @author Hillmer Chona
* @since 6.0.8
*/
public class AgeMinValidatorForThaiBuddhistDate extends AbstractAgeMinTimeBasedValidator<ThaiBuddhistDate> {

@Override
protected ThaiBuddhistDate getReferenceValue(Clock reference, int referenceAge, ChronoUnit unit) {
return ThaiBuddhistDate.now( reference ).minus( referenceAge, unit );
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* 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.age.min;

import java.time.Clock;
import java.time.LocalDateTime;
import java.time.Year;
import java.time.temporal.ChronoUnit;

/**
* Checks that the number of Years, Days, Months, etc. according to an unit {@code java.time.temporal.ChronoUnit}
* from a given {@code java.time.Year} to current day is greater than or equal to the specified value if inclusive is true
* or is greater when inclusive is false.
*
* @author Hillmer Chona
* @since 6.0.8
*/
public class AgeMinValidatorForYear extends AbstractAgeMinTimeBasedValidator<Year> {

@Override
protected Year getReferenceValue(Clock reference, int referenceAge, ChronoUnit unit) {
return Year.from( LocalDateTime.now( reference ).minus( referenceAge, unit ) );
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* 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.age.min;

import java.time.Clock;
import java.time.YearMonth;
import java.time.temporal.ChronoUnit;

/**
* Checks that the number of Years, Days, Months, etc. according to an unit {@code java.time.temporal.ChronoUnit}
* from a given {@code java.time.YearMonth} to current day is greater than or equal to the specified value if inclusive is true
* or is greater when inclusive is false.
*
* @author Hillmer Chona
* @since 6.0.8
*/
public class AgeMinValidatorForYearMonth extends AbstractAgeMinTimeBasedValidator<YearMonth> {

@Override
protected YearMonth getReferenceValue(Clock reference, int referenceAge, ChronoUnit unit) {
return YearMonth.now( reference ).minus( referenceAge, unit );
}
}
Original file line number Diff line number Diff line change
@@ -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;
@@ -266,6 +267,15 @@
import org.hibernate.validator.internal.constraintvalidators.hv.ScriptAssertValidator;
import org.hibernate.validator.internal.constraintvalidators.hv.URLValidator;
import org.hibernate.validator.internal.constraintvalidators.hv.UniqueElementsValidator;
import org.hibernate.validator.internal.constraintvalidators.hv.age.min.AgeMinValidatorForCalendar;
import org.hibernate.validator.internal.constraintvalidators.hv.age.min.AgeMinValidatorForDate;
import org.hibernate.validator.internal.constraintvalidators.hv.age.min.AgeMinValidatorForHijrahDate;
import org.hibernate.validator.internal.constraintvalidators.hv.age.min.AgeMinValidatorForJapaneseDate;
import org.hibernate.validator.internal.constraintvalidators.hv.age.min.AgeMinValidatorForLocalDate;
import org.hibernate.validator.internal.constraintvalidators.hv.age.min.AgeMinValidatorForMinguoDate;
import org.hibernate.validator.internal.constraintvalidators.hv.age.min.AgeMinValidatorForThaiBuddhistDate;
import org.hibernate.validator.internal.constraintvalidators.hv.age.min.AgeMinValidatorForYear;
import org.hibernate.validator.internal.constraintvalidators.hv.age.min.AgeMinValidatorForYearMonth;
import org.hibernate.validator.internal.constraintvalidators.hv.br.CNPJValidator;
import org.hibernate.validator.internal.constraintvalidators.hv.br.CPFValidator;
import org.hibernate.validator.internal.constraintvalidators.hv.pl.NIPValidator;
@@ -462,6 +472,19 @@ public ConstraintHelper() {
) );
}

List<Class<? extends ConstraintValidator<AgeMin, ?>>> ageMinValidators = new ArrayList<>( 9 );
ageMinValidators.add( AgeMinValidatorForCalendar.class );
ageMinValidators.add( AgeMinValidatorForDate.class );
ageMinValidators.add( AgeMinValidatorForHijrahDate.class );
ageMinValidators.add( AgeMinValidatorForJapaneseDate.class );
ageMinValidators.add( AgeMinValidatorForLocalDate.class );
ageMinValidators.add( AgeMinValidatorForMinguoDate.class );
ageMinValidators.add( AgeMinValidatorForThaiBuddhistDate.class );
ageMinValidators.add( AgeMinValidatorForYear.class );
ageMinValidators.add( AgeMinValidatorForYearMonth.class );
putConstraints( tmpConstraints, AgeMin.class, ageMinValidators );


if ( isJavaMoneyInClasspath() ) {
putConstraints( tmpConstraints, Negative.class, Arrays.asList(
NegativeValidatorForBigDecimal.class,
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* 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.constraints.annotations.hv.age;

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 java.time.LocalDate;
import java.util.Set;

import javax.validation.ConstraintViolation;

import org.hibernate.validator.constraints.AgeMin;
import org.hibernate.validator.test.constraints.annotations.AbstractConstrainedTest;
import org.hibernate.validator.testutil.TestForIssue;

import org.testng.annotations.Test;

/**
* Test to make sure that elements annotated with {@link AgeMin} are validated.
*
* @author Hillmer Chona
* @since 6.0.8
*/
@TestForIssue(jiraKey = "HV-1552")
public class AgeValidatorConstrainedTest extends AbstractConstrainedTest {

private static final int MINIMUM_AGE = 18;

@Test
public void testMinAge() {
LocalDate todayMinus18Years = LocalDate.now().minusYears( MINIMUM_AGE );
Foo foo = new Foo( todayMinus18Years );
Set<ConstraintViolation<Foo>> violations = validator.validate( foo );
assertNoViolations( violations );
}

@Test
public void testMinAgeInvalid() {
LocalDate tomorrowMinus18Years = LocalDate.now().plusDays( 1 ).minusYears( MINIMUM_AGE );
Foo foo = new Foo( tomorrowMinus18Years );
Set<ConstraintViolation<Foo>> violations = validator.validate( foo );
assertThat( violations ).containsOnlyViolations(
violationOf( AgeMin.class ).withMessage( "must be older than " + MINIMUM_AGE )
);
}

private static class Foo {

@AgeMin(value = MINIMUM_AGE)
private final LocalDate birthDate;

public Foo(LocalDate birthDate) {
this.birthDate = birthDate;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,348 @@
/*
* 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.age;

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.time.LocalDate;
import java.time.Year;
import java.time.chrono.JapaneseDate;
import java.time.chrono.MinguoDate;
import java.time.temporal.ChronoUnit;
import java.util.Calendar;
import java.util.Date;
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.age.min.AgeMinValidatorForCalendar;
import org.hibernate.validator.internal.constraintvalidators.hv.age.min.AgeMinValidatorForDate;
import org.hibernate.validator.internal.constraintvalidators.hv.age.min.AgeMinValidatorForJapaneseDate;
import org.hibernate.validator.internal.constraintvalidators.hv.age.min.AgeMinValidatorForLocalDate;
import org.hibernate.validator.internal.constraintvalidators.hv.age.min.AgeMinValidatorForMinguoDate;
import org.hibernate.validator.internal.constraintvalidators.hv.age.min.AgeMinValidatorForYear;
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;

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

private static final int MINIMUM_AGE_YEARS = 18;

private static final int MINIMUM_AGE_MONTHS = 240;

@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( MINIMUM_AGE_YEARS ) );
config.addMapping( mapping );
Validator validator = config.buildValidatorFactory().getValidator();

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

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

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


@Test
public void testLocalDate() throws Exception {

ConstraintValidator<AgeMin, LocalDate> constraintValidator = getInitializedValidator( new AgeMinValidatorForLocalDate(),
MINIMUM_AGE_YEARS, true
);

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

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

@Test
public void testInclusiveLocalDate() throws Exception {

ConstraintValidator<AgeMin, LocalDate> constraintValidatorInclusiveTrue = getInitializedValidator(
new AgeMinValidatorForLocalDate(),
MINIMUM_AGE_YEARS,
true
);
ConstraintValidator<AgeMin, LocalDate> constraintValidatorInclusiveFalse = getInitializedValidator(
new AgeMinValidatorForLocalDate(),
MINIMUM_AGE_YEARS,
false
);

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

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

@Test
public void testLocalDateChronoUnits() throws Exception {
ConstraintValidator<AgeMin, LocalDate> constraintValidator = getInitializedValidatorWithUnit(
new AgeMinValidatorForLocalDate(),
MINIMUM_AGE_MONTHS,
true,
AgeMin.Unit.MONTHS
);

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

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

@Test
public void testCalendar() throws Exception {

ConstraintValidator<AgeMin, Calendar> constraintValidator = getInitializedValidator(
new AgeMinValidatorForCalendar(),
MINIMUM_AGE_YEARS,
true
);

Calendar todayMinus18Years = getCalendarTodayMinus18Years();

Calendar todayMinus2MonthAnd18Years = getCalendarTodayMinus18Years();
todayMinus2MonthAnd18Years.add( Calendar.MONTH, 2 * -1 );

Calendar tomorrowMinus18Years = getCalendarTodayMinus18Years();
tomorrowMinus18Years.add( Calendar.DATE, 1 );

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

@Test
public void testInclusiveCalendar() throws Exception {
ConstraintValidator<AgeMin, Calendar> constraintValidatorInclusiveTrue = getInitializedValidator(
new AgeMinValidatorForCalendar(),
MINIMUM_AGE_YEARS,
true
);
ConstraintValidator<AgeMin, Calendar> constraintValidatorInclusiveFalse = getInitializedValidator(
new AgeMinValidatorForCalendar(),
MINIMUM_AGE_YEARS,
false
);

Calendar todayMinus18Years = getCalendarTodayMinus18Years();

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

@Test
public void testDate() throws Exception {

ConstraintValidator<AgeMin, Date> constraintValidator = getInitializedValidator(
new AgeMinValidatorForDate(),
MINIMUM_AGE_YEARS,
true
);

Calendar todayMinus18Years = getCalendarTodayMinus18Years();

Calendar todayMinus2MonthAnd18Years = getCalendarTodayMinus18Years();
todayMinus2MonthAnd18Years.add( Calendar.MONTH, 2 * -1 );

Calendar tomorrowMinus18Years = getCalendarTodayMinus18Years();
tomorrowMinus18Years.add( Calendar.DATE, 1 );

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

@Test
public void testJapaneseDate() throws Exception {

ConstraintValidator<AgeMin, JapaneseDate> constraintValidator = getInitializedValidator(
new AgeMinValidatorForJapaneseDate(),
MINIMUM_AGE_YEARS,
true
);

JapaneseDate todayMinus18Years = JapaneseDate.now().minus( MINIMUM_AGE_YEARS, ChronoUnit.YEARS );
JapaneseDate todayMinus2MonthAnd18Years = JapaneseDate.now().minus( 2, ChronoUnit.MONTHS )
.minus( MINIMUM_AGE_YEARS, ChronoUnit.YEARS );
JapaneseDate tomorrowMinus18Years = JapaneseDate.now().plus( 1, ChronoUnit.DAYS )
.minus( MINIMUM_AGE_YEARS, ChronoUnit.YEARS );

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

@Test
public void testMinguoDate() throws Exception {

ConstraintValidator<AgeMin, MinguoDate> constraintValidator = getInitializedValidator(
new AgeMinValidatorForMinguoDate(),
MINIMUM_AGE_YEARS,
true
);

MinguoDate todayMinus18Years = MinguoDate.now().minus( MINIMUM_AGE_YEARS, ChronoUnit.YEARS );
MinguoDate todayMinus2MonthAnd18Years = MinguoDate.now().minus( 2, ChronoUnit.MONTHS )
.minus( MINIMUM_AGE_YEARS, ChronoUnit.YEARS );
MinguoDate tomorrowMinus18Years = MinguoDate.now().plus( 1, ChronoUnit.DAYS )
.minus( MINIMUM_AGE_YEARS, ChronoUnit.YEARS );

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

@Test
public void testYear() throws Exception {

boolean inclusive = true;
ConstraintValidator<AgeMin, Year> constraintValidator = getInitializedValidatorWithUnit(
new AgeMinValidatorForYear(),
MINIMUM_AGE_YEARS,
inclusive,
AgeMin.Unit.MONTHS
);

Year todayMinus18Years = Year.from( LocalDate.now()
.minus( MINIMUM_AGE_YEARS, AgeMin.Unit.MONTHS.getChronoUnit() ) );
Year nextYearMinus18Years = Year.from( LocalDate.now()
.plus( 1, ChronoUnit.YEARS )
.minus(
MINIMUM_AGE_YEARS,
AgeMin.Unit.MONTHS.getChronoUnit()
) );


assertValidAge( null, constraintValidator );
assertValidAge( todayMinus18Years, constraintValidator );
assertInvalidAge( nextYearMinus18Years, constraintValidator );
}

private Calendar getCalendarTodayMinus18Years() {

Calendar calendar = Calendar.getInstance();

calendar.clear( Calendar.HOUR_OF_DAY );
calendar.clear( Calendar.MINUTE );
calendar.clear( Calendar.SECOND );
calendar.clear( Calendar.MILLISECOND );

calendar.add( Calendar.YEAR, MINIMUM_AGE_YEARS * -1 );

return calendar;

}

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

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;
}

/**
* @return an initialized {@link ConstraintValidator} using {@code DUMMY_CONSTRAINT_VALIDATOR_INITIALIZATION_CONTEXT}
*/
private <T> ConstraintValidator<AgeMin, T> getInitializedValidatorWithUnit(
HibernateConstraintValidator<AgeMin, T> validator,
int value, boolean inclusive, AgeMin.Unit unit) {

ConstraintAnnotationDescriptor.Builder<AgeMin> descriptorBuilder = new ConstraintAnnotationDescriptor.Builder<>(
AgeMin.class );
descriptorBuilder.setAttribute( "value", value );
descriptorBuilder.setAttribute( "inclusive", inclusive );
descriptorBuilder.setAttribute( "unit", unit );

ConstraintAnnotationDescriptor<AgeMin> descriptor = descriptorBuilder.build();
ConstraintValidatorInitializationHelper.initialize( validator, descriptor );
return validator;
}

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

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

private static class User {
private final LocalDate birthDate;

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