Skip to content

HV-1970 Add Korean specific RRN annotation #1338

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

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
bafee67
HV-1970 Adding KorRRN annotation
ing9990 Jan 12, 2024
e4d3227
HV-1970 Adding KorRRN validator tests
ing9990 Jan 12, 2024
123f252
HV-1970 Refactoring to the Hibernate Code Style
ing9990 Jan 12, 2024
a672e04
HV-1970 Remove type field
ing9990 Jan 12, 2024
98a5164
HV-1970 Encoding Javadoc url
ing9990 Jan 14, 2024
ec01653
HV-1970 Adding RRN validation criteria and modifying test code
ing9990 Jan 15, 2024
883f3ef
HV-1970 Encoding javadoc url
ing9990 Jan 15, 2024
103eac2
HV-1970 Adding test cases and refactoring the RRNValidator
ing9990 Jan 15, 2024
00ddc91
HV-1970 fix error
ing9990 Jan 15, 2024
a3f45bc
HV-1970 Add attribute to optionally validate check-digit, adding and …
ing9990 Jan 16, 2024
f60784f
HV-1970 Remove private signature for Java8 version
ing9990 Jan 16, 2024
d0962dc
HV-1970 Change 'if' statement to 'switch' statement for Readability,…
ing9990 Jan 16, 2024
5f1337b
HV-1970 Change 'switch' statement to 'if' statement for Java 11
ing9990 Jan 16, 2024
f8e4c09
HV-1970 Change attribute name from BEFORE_OCTOBER_2020_ONLY to ALWAYS
ing9990 Jan 16, 2024
c625080
HV-1970 Adding test case for default value of validateCheckDigit()
ing9990 Jan 24, 2024
9566047
HV-1970 Fix typos
ing9990 Jan 24, 2024
7962786
HV-1970 Refactor: Moving validation methods to RRNValidationAlgorithm…
ing9990 Jan 24, 2024
b8951fb
HV-1970 Fix typo
ing9990 Jan 24, 2024
c589eb0
HV-1970 Adding asciidoc for KorRRN Annotation
ing9990 Jan 24, 2024
974c616
HV-1970 Modifying test case to pass the test
ing9990 Jan 24, 2024
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
Expand Up @@ -303,6 +303,7 @@ public ConstraintHelper(Types typeUtils, AnnotationApiHelper annotationApiHelper
registerAllowedTypesForBuiltInConstraint( HibernateValidatorTypes.NIP_CHECK, CharSequence.class );
registerAllowedTypesForBuiltInConstraint( HibernateValidatorTypes.PESEL_CHECK, CharSequence.class );
registerAllowedTypesForBuiltInConstraint( HibernateValidatorTypes.INN_CHECK, CharSequence.class );
registerAllowedTypesForBuiltInConstraint( HibernateValidatorTypes.KOR_RRN_CHECK, CharSequence.class );
registerAllowedTypesForBuiltInConstraint( HibernateValidatorTypes.NOT_BLANK, CharSequence.class );
registerAllowedTypesForBuiltInConstraint( HibernateValidatorTypes.NOT_EMPTY, TYPES_SUPPORTED_BY_SIZE_AND_NOT_EMPTY_ANNOTATIONS );
registerAllowedTypesForBuiltInConstraint( HibernateValidatorTypes.NORMALIZED, CharSequence.class );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ public static class HibernateValidatorTypes {
public static final String NIP_CHECK = ORG_HIBERNATE_VALIDATOR_CONSTRAINTS + ".pl.NIP";
public static final String PESEL_CHECK = ORG_HIBERNATE_VALIDATOR_CONSTRAINTS + ".pl.PESEL";
public static final String INN_CHECK = ORG_HIBERNATE_VALIDATOR_CONSTRAINTS + ".ru.INN";
public static final String KOR_RRN_CHECK = ORG_HIBERNATE_VALIDATOR_CONSTRAINTS + "kor.KorRRN";
public static final String NORMALIZED = ORG_HIBERNATE_VALIDATOR_CONSTRAINTS + ".Normalized";
public static final String UUID = ORG_HIBERNATE_VALIDATOR_CONSTRAINTS + ".UUID";
public static final String NOT_BLANK = ORG_HIBERNATE_VALIDATOR_CONSTRAINTS + ".NotBlank";
Expand Down
5 changes: 5 additions & 0 deletions documentation/src/main/asciidoc/ch02.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,11 @@ Hibernate Validator!
Hibernate metadata impact::: None
Country::: Russia

`@KorRRN`:: Checks that the annotated character sequence represents a Korean resident registration number (https://ko.wikipedia.org/wiki/%EC%A3%BC%EB%AF%BC%EB%93%B1%EB%A1%9D%EB%B2%88%ED%98%B8[KorRRN])
Supported data types::: `CharSequence`
Hibernate metadata impact::: None
Country::: South Korea

[TIP]
====
In some cases neither the Jakarta Bean Validation constraints nor the custom constraints provided by
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* 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.kor;

import org.hibernate.validator.cfg.ConstraintDef;
import org.hibernate.validator.constraints.kor.KorRRN;

public class KorRRNDef extends ConstraintDef<KorRRNDef, KorRRN> {

public KorRRNDef() {
super( KorRRN.class );
}

public KorRRNDef validateCheckDigit(KorRRN.ValidateCheckDigit validateCheckDigit) {
addParameter( "validateCheckDigit", validateCheckDigit );
return this;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* 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>.
*/

/**
* <p>Korean specific constraint definition classes for programmatic constraint definition API.</p>
* <p>This package is part of the public Hibernate Validator API.</p>
*/
package org.hibernate.validator.cfg.defs.kor;
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* 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.kor;

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;
import static org.hibernate.validator.constraints.kor.KorRRN.ValidateCheckDigit.NEVER;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import jakarta.validation.ReportAsSingleViolation;

import java.lang.annotation.Documented;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

/**
* Checks that the annotated character sequence is a valid Korean resident registration number.
*
* @author Taewoo Kim
* @see <a href="https://ko.wikipedia.org/wiki/%EC%A3%BC%EB%AF%BC%EB%93%B1%EB%A1%9D%EB%B2%88%ED%98%B8">Korean resident registration number</a>
*/

@Documented
@Constraint(validatedBy = {})
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(KorRRN.List.class)
@ReportAsSingleViolation
public @interface KorRRN {

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

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

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

ValidateCheckDigit validateCheckDigit() default NEVER;

/**
* Defines the validation rules this constraint applies to the Korean resident registration number.
* <p>
* Each type specifies which particular checks will be applied.
*/
enum ValidateCheckDigit {
/**
* Perform the checks on:
* <ul>
* <li>The length of an RRN</li>
* <li>The validity of the Gender-Digit in an RRN</li>
* <li>The validity of the date in an RRN</li>
* </ul>
*/
NEVER,
/**
* Perform the checks on:
* <ul>
* <li>The length of an RRN</li>
* <li>The validity of the Gender-Digit in an RRN</li>
* <li>The validity of the date in an RRN</li>
* <li>The Validity of check-digit in RRN</li>
* </ul>
*/
ALWAYS
}

/**
* Defines several {@code @KorRRN} annotations on the same element.
*/
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@interface List {
KorRRN[] value();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@

/*
* 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.kor;

import java.util.ArrayList;
import java.util.List;

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

import org.hibernate.validator.constraints.kor.KorRRN;
import org.hibernate.validator.internal.util.Contracts;
import org.hibernate.validator.internal.util.ModUtil;

/**
* Checks that a given character sequence is a valid RRN.
*
* @author Taewoo Kim
* @see <a href="https://www.law.go.kr/LSW/lsInfoP.do?lsId=008230&ancYnChk=0#0000">Korean Resident Registration Act Implementation Rules</a>
*/
public class KorRRNValidator implements ConstraintValidator<KorRRN, CharSequence> {

private static final List<Integer> GENDER_DIGIT = List.of( 1, 2, 3, 4 );
// Check sum weight for ModUtil
private static final int[] CHECK_SUM_WEIGHT = new int[] { 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2 };
// index of the digit representing the gender
private static final int GENDER_DIGIT_INDEX = 6;

private RRNValidationAlgorithm rrnValidationAlgorithm;

@Override
public void initialize(KorRRN constraintAnnotation) {
this.rrnValidationAlgorithm = RRNValidationAlgorithm.from( constraintAnnotation.validateCheckDigit() );
}

@Override
public boolean isValid(CharSequence rrnValue, ConstraintValidatorContext context) {
if ( rrnValue == null ) {
return true;
}
return rrnValidationAlgorithm.isValid( rrnValue.toString().replace( "-", "" ) );
}

private interface RRNValidationAlgorithm {
int VALID_LENGTH = 13;
int THRESHOLD = 9;
int MODULDO = 11;

boolean isValid(String rrn);

// Returns an implementation of the algorithm based on the value of ValidateCheckDigit
static RRNValidationAlgorithm from(KorRRN.ValidateCheckDigit validateCheckDigit) {
Contracts.assertNotNull( validateCheckDigit );
if ( validateCheckDigit == KorRRN.ValidateCheckDigit.ALWAYS ) {
return RRNValidationAlgorithmImpl.ALWAYS;
}
return RRNValidationAlgorithmImpl.NEVER;
}
}

private enum RRNValidationAlgorithmImpl implements RRNValidationAlgorithm {
NEVER {
@Override
public boolean isValid(String rrn) {
return isValidLength( rrn ) && isValidDate( rrn ) && isValidGenderDigit( rrn );
}
},
ALWAYS {
@Override
public boolean isValid(String rrn) {
return isValidLength( rrn ) && isValidDate( rrn ) && isValidGenderDigit( rrn ) && isValidChecksum( rrn );
}
};

// Check the check-digit of the RRN using ModUtil
boolean isValidChecksum(final String rrn) {
int checksum = ModUtil.calculateModXCheckWithWeights(
toChecksumDigits( rrn ),
MODULDO,
THRESHOLD,
CHECK_SUM_WEIGHT
);
checksum = checksum >= 10 ? checksum - 10 : checksum;
return checksum == getChectDigit( rrn );
}

boolean isValidDate(final String rrn) {
final int month = extractMonth( rrn );
final int day = extractDay( rrn );
if ( month > 12 || day < 0 || day > 31 ) {
return false;
}
return day <= 31 && ( day <= 30 || ( month != 4 && month != 6 && month != 9 && month != 11 ) ) && ( day <= 29 || month != 2 );
}

boolean isValidLength(String rrn) {
return rrn.length() == VALID_LENGTH;
}

boolean isValidGenderDigit(String rrn) {
return GENDER_DIGIT.contains( extractGenderDigit( rrn ) );
}

int extractGenderDigit(String rrn) {
return Character.getNumericValue( rrn.charAt( GENDER_DIGIT_INDEX ) );
}

List<Integer> toChecksumDigits(String rrn) {
List<Integer> collect = new ArrayList<>();
for ( int i = 0; i < rrn.length() - 1; i++ ) {
collect.add( Character.getNumericValue( rrn.charAt( i ) ) );
}
return collect;
}

int getChectDigit(String rrn) {
return Character.getNumericValue( rrn.charAt( rrn.length() - 1 ) );
}

int extractDay(String rrn) {
return Integer.parseInt( rrn.substring( 4, 6 ) );
}

int extractMonth(String rrn) {
return Integer.parseInt( rrn.substring( 2, 4 ) );
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ enum BuiltinConstraint {
Arrays.asList( JAKARTA_VALIDATION_CONSTRAINTS_PATTERN )),
ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_BR_TITULO_ELEITORAL("org.hibernate.validator.constraints.br.TituloEleitoral",
Arrays.asList( JAKARTA_VALIDATION_CONSTRAINTS_PATTERN, ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_MOD11_CHECK ) ),
ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_KOR_KORRRN("org.hibernate.validator.constraints.kor.KorRRN"),
ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_PL_NIP("org.hibernate.validator.constraints.pl.NIP"),
ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_PL_PESEL("org.hibernate.validator.constraints.pl.PESEL"),
ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_PL_REGON("org.hibernate.validator.constraints.pl.REGON"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import static org.hibernate.validator.internal.metadata.core.BuiltinConstraint.ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_EAN;
import static org.hibernate.validator.internal.metadata.core.BuiltinConstraint.ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_EMAIL;
import static org.hibernate.validator.internal.metadata.core.BuiltinConstraint.ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_ISBN;
import static org.hibernate.validator.internal.metadata.core.BuiltinConstraint.ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_KOR_KORRRN;
import static org.hibernate.validator.internal.metadata.core.BuiltinConstraint.ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_LENGTH;
import static org.hibernate.validator.internal.metadata.core.BuiltinConstraint.ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_LUHN_CHECK;
import static org.hibernate.validator.internal.metadata.core.BuiltinConstraint.ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_MOD10_CHECK;
Expand Down Expand Up @@ -99,6 +100,7 @@
import org.hibernate.validator.constraints.br.CNPJ;
import org.hibernate.validator.constraints.br.CPF;
import org.hibernate.validator.constraints.br.TituloEleitoral;
import org.hibernate.validator.constraints.kor.KorRRN;
import org.hibernate.validator.constraints.pl.NIP;
import org.hibernate.validator.constraints.pl.PESEL;
import org.hibernate.validator.constraints.pl.REGON;
Expand Down Expand Up @@ -317,6 +319,7 @@
import org.hibernate.validator.internal.constraintvalidators.hv.UniqueElementsValidator;
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.kor.KorRRNValidator;
import org.hibernate.validator.internal.constraintvalidators.hv.pl.NIPValidator;
import org.hibernate.validator.internal.constraintvalidators.hv.pl.PESELValidator;
import org.hibernate.validator.internal.constraintvalidators.hv.pl.REGONValidator;
Expand Down Expand Up @@ -801,6 +804,9 @@ private ConstraintHelper(Set<BuiltinConstraint> enabledBuiltinConstraints) {
if ( enabledBuiltinConstraints.contains( ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_RANGE ) ) {
putBuiltinConstraint( tmpConstraints, Range.class );
}
if ( enabledBuiltinConstraints.contains( ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_KOR_KORRRN ) ) {
putBuiltinConstraint( tmpConstraints, KorRRN.class, KorRRNValidator.class );
}
if ( enabledBuiltinConstraints.contains( ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_PL_REGON ) ) {
putBuiltinConstraint( tmpConstraints, REGON.class, REGONValidator.class );
}
Expand Down Expand Up @@ -1174,7 +1180,7 @@ private static <T> T run(PrivilegedAction<T> action) {

/**
* A type-safe wrapper around a concurrent map from constraint types to
* associated validator classes. The casts are safe as data is added trough
* associated validator classes. The casts are safe as data is added through
* the typed API only.
*
* @author Gunnar Morling
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ org.hibernate.validator.constraints.br.CNPJ.message = invalid Br
org.hibernate.validator.constraints.br.CPF.message = invalid Brazilian individual taxpayer registry number (CPF)
org.hibernate.validator.constraints.br.TituloEleitoral.message = invalid Brazilian Voter ID card number

org.hibernate.validator.constraints.kor.KorRRN.message = invalid Korean resident registration number (KorRRN)

org.hibernate.validator.constraints.pl.REGON.message = invalid Polish Taxpayer Identification Number (REGON)
org.hibernate.validator.constraints.pl.NIP.message = invalid VAT Identification Number (NIP)
org.hibernate.validator.constraints.pl.PESEL.message = invalid Polish National Identification Number (PESEL)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import org.hibernate.validator.cfg.defs.br.CNPJDef;
import org.hibernate.validator.cfg.defs.br.CPFDef;
import org.hibernate.validator.cfg.defs.br.TituloEleitoralDef;
import org.hibernate.validator.cfg.defs.kor.KorRRNDef;
import org.hibernate.validator.cfg.defs.pl.NIPDef;
import org.hibernate.validator.cfg.defs.pl.PESELDef;
import org.hibernate.validator.cfg.defs.pl.REGONDef;
Expand All @@ -32,6 +33,7 @@
import org.hibernate.validator.constraints.br.CNPJ;
import org.hibernate.validator.constraints.br.CPF;
import org.hibernate.validator.constraints.br.TituloEleitoral;
import org.hibernate.validator.constraints.kor.KorRRN;
import org.hibernate.validator.constraints.pl.NIP;
import org.hibernate.validator.constraints.pl.PESEL;
import org.hibernate.validator.constraints.pl.REGON;
Expand Down Expand Up @@ -64,6 +66,10 @@ public void countrySpecificProgrammaticDefinition() {

doProgrammaticTest( INN.class, new INNDef().type( INN.Type.INDIVIDUAL ), "127530851622", "127530851623", "invalid Russian taxpayer identification number (INN)" );
doProgrammaticTest( INN.class, new INNDef().type( INN.Type.JURIDICAL ), "8606995694", "8606995695", "invalid Russian taxpayer identification number (INN)" );

doProgrammaticTest( KorRRN.class, new KorRRNDef().validateCheckDigit( KorRRN.ValidateCheckDigit.NEVER ), "030205-1000004", "010199-1063015", "invalid Korean resident registration number (KorRRN)" );
doProgrammaticTest( KorRRN.class, new KorRRNDef().validateCheckDigit( KorRRN.ValidateCheckDigit.ALWAYS ), "030205-2567485", "030299-5000000", "invalid Korean resident registration number (KorRRN)" );
doProgrammaticTest( KorRRN.class, new KorRRNDef(),"030205-1000004", "010199-1063015", "invalid Korean resident registration number (KorRRN)" );
}

@Test
Expand Down
Loading