From c3f1f687c4b1544e24ea569fd1fc73580d287419 Mon Sep 17 00:00:00 2001 From: Detinho Date: Fri, 21 Jun 2024 20:41:28 -0300 Subject: [PATCH] HV-1999 new CNPJ alphanumeric format --- .../validator/constraints/Mod11Check.java | 6 ++ .../validator/constraints/br/CNPJ.java | 4 +- .../hv/Mod11CheckValidator.java | 7 +- .../constraintvalidators/hv/ModCheckBase.java | 45 ++++++++++-- .../hv/br/CNPJValidator.java | 11 +-- .../hv/br/CPFValidator.java | 12 +-- .../validator/internal/util/logging/Log.java | 3 + .../annotations/hv/br/CNPJValidatorTest.java | 73 +++++++++++++++++++ .../hv/Mod11CheckValidatorTest.java | 63 ++++++++++++---- 9 files changed, 188 insertions(+), 36 deletions(-) diff --git a/engine/src/main/java/org/hibernate/validator/constraints/Mod11Check.java b/engine/src/main/java/org/hibernate/validator/constraints/Mod11Check.java index 01b841e15d..a98865424a 100644 --- a/engine/src/main/java/org/hibernate/validator/constraints/Mod11Check.java +++ b/engine/src/main/java/org/hibernate/validator/constraints/Mod11Check.java @@ -121,6 +121,12 @@ */ ProcessingDirection processingDirection() default ProcessingDirection.RIGHT_TO_LEFT; + /** + * @return Whether upper case letters characters in the validated input should be allowed ({@code true}) or result in a + * validation error ({@code false}). + */ + boolean alphanumeric() default false; + /** * Defines several {@code @Mod11Check} annotations on the same element. */ diff --git a/engine/src/main/java/org/hibernate/validator/constraints/br/CNPJ.java b/engine/src/main/java/org/hibernate/validator/constraints/br/CNPJ.java index 0bc9226576..581bd6d548 100644 --- a/engine/src/main/java/org/hibernate/validator/constraints/br/CNPJ.java +++ b/engine/src/main/java/org/hibernate/validator/constraints/br/CNPJ.java @@ -31,7 +31,7 @@ * * @author George Gastaldi */ -@Pattern(regexp = "([0-9]{2}[.]?[0-9]{3}[.]?[0-9]{3}[/]?[0-9]{4}[-]?[0-9]{2})") +@Pattern(regexp = "([0-9A-Z]{2}[.]?[0-9A-Z]{3}[.]?[0-9A-Z]{3}[/]?[0-9A-Z]{4}[-]?[0-9]{2})") @ReportAsSingleViolation @Documented @Constraint(validatedBy = { }) @@ -45,6 +45,8 @@ Class[] payload() default { }; + boolean alphanumeric() default false; + /** * Defines several {@code @CNPJ} annotations on the same element. */ diff --git a/engine/src/main/java/org/hibernate/validator/internal/constraintvalidators/hv/Mod11CheckValidator.java b/engine/src/main/java/org/hibernate/validator/internal/constraintvalidators/hv/Mod11CheckValidator.java index 4948e31a9c..df0655939c 100644 --- a/engine/src/main/java/org/hibernate/validator/internal/constraintvalidators/hv/Mod11CheckValidator.java +++ b/engine/src/main/java/org/hibernate/validator/internal/constraintvalidators/hv/Mod11CheckValidator.java @@ -65,7 +65,8 @@ public void initialize(Mod11Check constraintAnnotation) { constraintAnnotation.threshold(), constraintAnnotation.treatCheck10As(), constraintAnnotation.treatCheck11As(), - constraintAnnotation.processingDirection() + constraintAnnotation.processingDirection(), + constraintAnnotation.alphanumeric() ); } @@ -78,6 +79,7 @@ public void initialize( char treatCheck10As, char treatCheck11As, ProcessingDirection direction, + boolean alphanumeric, int... customWeights ) { @@ -85,7 +87,8 @@ public void initialize( startIndex, endIndex, checkDigitIndex, - ignoreNonDigitCharacters + ignoreNonDigitCharacters, + alphanumeric ); this.threshold = threshold; this.reverseOrder = direction == ProcessingDirection.LEFT_TO_RIGHT; diff --git a/engine/src/main/java/org/hibernate/validator/internal/constraintvalidators/hv/ModCheckBase.java b/engine/src/main/java/org/hibernate/validator/internal/constraintvalidators/hv/ModCheckBase.java index dfb27308ad..7f1c1cd863 100644 --- a/engine/src/main/java/org/hibernate/validator/internal/constraintvalidators/hv/ModCheckBase.java +++ b/engine/src/main/java/org/hibernate/validator/internal/constraintvalidators/hv/ModCheckBase.java @@ -29,8 +29,10 @@ public abstract class ModCheckBase { private static final Log LOG = LoggerFactory.make( MethodHandles.lookup() ); private static final Pattern NUMBERS_ONLY_REGEXP = Pattern.compile( "[^0-9]" ); + private static final Pattern NUMBERS_UPPER_LETTERS_ONLY_REGEXP = Pattern.compile( "[^0-9A-Z]" ); private static final int DEC_RADIX = 10; + private static final int BASE_CHAR_INDEX = 48; /** * The start index for the checksum calculation @@ -47,7 +49,8 @@ public abstract class ModCheckBase { */ private int checkDigitIndex; - private boolean ignoreNonDigitCharacters; + private boolean ignoreNonValidCharacters; + private boolean alphanumeric; public boolean isValid(final CharSequence value, final ConstraintValidatorContext context) { if ( value == null ) { @@ -79,11 +82,22 @@ public boolean isValid(final CharSequence value, final ConstraintValidatorContex public abstract boolean isCheckDigitValid(List digits, char checkDigit); - protected void initialize(int startIndex, int endIndex, int checkDigitIndex, boolean ignoreNonDigitCharacters) { + protected void initialize(int startIndex, int endIndex, int checkDigitIndex, boolean ignoreNonValidCharacters) { this.startIndex = startIndex; this.endIndex = endIndex; this.checkDigitIndex = checkDigitIndex; - this.ignoreNonDigitCharacters = ignoreNonDigitCharacters; + this.ignoreNonValidCharacters = ignoreNonValidCharacters; + this.alphanumeric = false; + + this.validateOptions(); + } + + protected void initialize(int startIndex, int endIndex, int checkDigitIndex, boolean ignoreNonValidCharacters, boolean alphanumeric) { + this.startIndex = startIndex; + this.endIndex = endIndex; + this.checkDigitIndex = checkDigitIndex; + this.ignoreNonValidCharacters = ignoreNonValidCharacters; + this.alphanumeric = alphanumeric; this.validateOptions(); } @@ -98,11 +112,21 @@ protected void initialize(int startIndex, int endIndex, int checkDigitIndex, boo * @throws NumberFormatException in case character is not a digit */ protected int extractDigit(char value) throws NumberFormatException { - if ( Character.isDigit( value ) ) { - return Character.digit( value, DEC_RADIX ); + if ( alphanumeric ) { + if ( (value >= '0' && value <= '9' ) || (value >= 'A' && value <= 'Z') ) { + return value - BASE_CHAR_INDEX; + } + else { + throw LOG.getCharacterIsNotADigitOrUpperCaseLetterException( value ); + } } else { - throw LOG.getCharacterIsNotADigitException( value ); + if ( Character.isDigit( value ) ) { + return Character.digit( value, DEC_RADIX ); + } + else { + throw LOG.getCharacterIsNotADigitException( value ); + } } } @@ -145,8 +169,13 @@ private boolean validateOptions() { } private String stripNonDigitsIfRequired(String value) { - if ( ignoreNonDigitCharacters ) { - return NUMBERS_ONLY_REGEXP.matcher( value ).replaceAll( "" ); + if ( ignoreNonValidCharacters ) { + if ( alphanumeric ) { + return NUMBERS_UPPER_LETTERS_ONLY_REGEXP.matcher( value ).replaceAll( "" ); + } + else { + return NUMBERS_ONLY_REGEXP.matcher( value ).replaceAll( "" ); + } } else { return value; diff --git a/engine/src/main/java/org/hibernate/validator/internal/constraintvalidators/hv/br/CNPJValidator.java b/engine/src/main/java/org/hibernate/validator/internal/constraintvalidators/hv/br/CNPJValidator.java index 92a206c87a..22e4a43330 100644 --- a/engine/src/main/java/org/hibernate/validator/internal/constraintvalidators/hv/br/CNPJValidator.java +++ b/engine/src/main/java/org/hibernate/validator/internal/constraintvalidators/hv/br/CNPJValidator.java @@ -20,6 +20,7 @@ */ public class CNPJValidator implements ConstraintValidator { private static final Pattern DIGITS_ONLY = Pattern.compile( "\\d+" ); + private static final Pattern NUMBERS_UPPER_LETTERS_ONLY_REGEXP = Pattern.compile( "[0-9A-Z]+" ); private final Mod11CheckValidator withSeparatorMod11Validator1 = new Mod11CheckValidator(); private final Mod11CheckValidator withSeparatorMod11Validator2 = new Mod11CheckValidator(); @@ -34,19 +35,19 @@ public void initialize(CNPJ constraintAnnotation) { // check digit being the digit directly after the hyphen. The second checksum is over all digits // pre hyphen + first check digit. The check digit in this case is the second digit after the hyphen withSeparatorMod11Validator1.initialize( - 0, 14, 16, true, 9, '0', '0', Mod11Check.ProcessingDirection.RIGHT_TO_LEFT + 0, 14, 16, true, 9, '0', '0', Mod11Check.ProcessingDirection.RIGHT_TO_LEFT, constraintAnnotation.alphanumeric() ); withSeparatorMod11Validator2.initialize( - 0, 16, 17, true, 9, '0', '0', Mod11Check.ProcessingDirection.RIGHT_TO_LEFT + 0, 16, 17, true, 9, '0', '0', Mod11Check.ProcessingDirection.RIGHT_TO_LEFT, constraintAnnotation.alphanumeric() ); // validates CNPJ strings without separator, eg 91509901000169 // checksums as described above, except there are no separator characters withoutSeparatorMod11Validator1.initialize( - 0, 11, 12, true, 9, '0', '0', Mod11Check.ProcessingDirection.RIGHT_TO_LEFT + 0, 11, 12, true, 9, '0', '0', Mod11Check.ProcessingDirection.RIGHT_TO_LEFT, constraintAnnotation.alphanumeric() ); withoutSeparatorMod11Validator2.initialize( - 0, 12, 13, true, 9, '0', '0', Mod11Check.ProcessingDirection.RIGHT_TO_LEFT + 0, 12, 13, true, 9, '0', '0', Mod11Check.ProcessingDirection.RIGHT_TO_LEFT, constraintAnnotation.alphanumeric() ); } @@ -72,7 +73,7 @@ public boolean isValid(CharSequence value, ConstraintValidatorContext context) { return false; } - if ( DIGITS_ONLY.matcher( value ).matches() ) { + if ( NUMBERS_UPPER_LETTERS_ONLY_REGEXP.matcher( value ).matches() ) { return withoutSeparatorMod11Validator1.isValid( value, context ) && withoutSeparatorMod11Validator2.isValid( value, context ); } diff --git a/engine/src/main/java/org/hibernate/validator/internal/constraintvalidators/hv/br/CPFValidator.java b/engine/src/main/java/org/hibernate/validator/internal/constraintvalidators/hv/br/CPFValidator.java index 3488ea4e51..c1fa9a6efb 100644 --- a/engine/src/main/java/org/hibernate/validator/internal/constraintvalidators/hv/br/CPFValidator.java +++ b/engine/src/main/java/org/hibernate/validator/internal/constraintvalidators/hv/br/CPFValidator.java @@ -37,27 +37,27 @@ public void initialize(CPF constraintAnnotation) { // check digit being the digit directly after the hyphen. The second checksum is over all digits // pre hyphen + first check digit. The check digit in this case is the second digit after the hyphen withSeparatorMod11Validator1.initialize( - 0, 10, 12, true, Integer.MAX_VALUE, '0', '0', Mod11Check.ProcessingDirection.RIGHT_TO_LEFT + 0, 10, 12, true, Integer.MAX_VALUE, '0', '0', Mod11Check.ProcessingDirection.RIGHT_TO_LEFT, false ); withSeparatorMod11Validator2.initialize( - 0, 12, 13, true, Integer.MAX_VALUE, '0', '0', Mod11Check.ProcessingDirection.RIGHT_TO_LEFT + 0, 12, 13, true, Integer.MAX_VALUE, '0', '0', Mod11Check.ProcessingDirection.RIGHT_TO_LEFT, false ); // validates CPF strings with separator, eg 134241313-00 withDashOnlySeparatorMod11Validator1.initialize( - 0, 8, 10, true, Integer.MAX_VALUE, '0', '0', Mod11Check.ProcessingDirection.RIGHT_TO_LEFT + 0, 8, 10, true, Integer.MAX_VALUE, '0', '0', Mod11Check.ProcessingDirection.RIGHT_TO_LEFT, false ); withDashOnlySeparatorMod11Validator2.initialize( - 0, 10, 11, true, Integer.MAX_VALUE, '0', '0', Mod11Check.ProcessingDirection.RIGHT_TO_LEFT + 0, 10, 11, true, Integer.MAX_VALUE, '0', '0', Mod11Check.ProcessingDirection.RIGHT_TO_LEFT, false ); // validates CPF strings without separator, eg 13424131300 // checksums as described above, except there are no separator characters withoutSeparatorMod11Validator1.initialize( - 0, 8, 9, true, Integer.MAX_VALUE, '0', '0', Mod11Check.ProcessingDirection.RIGHT_TO_LEFT + 0, 8, 9, true, Integer.MAX_VALUE, '0', '0', Mod11Check.ProcessingDirection.RIGHT_TO_LEFT, false ); withoutSeparatorMod11Validator2.initialize( - 0, 9, 10, true, Integer.MAX_VALUE, '0', '0', Mod11Check.ProcessingDirection.RIGHT_TO_LEFT + 0, 9, 10, true, Integer.MAX_VALUE, '0', '0', Mod11Check.ProcessingDirection.RIGHT_TO_LEFT, false ); } diff --git a/engine/src/main/java/org/hibernate/validator/internal/util/logging/Log.java b/engine/src/main/java/org/hibernate/validator/internal/util/logging/Log.java index d61b736f14..81342021b3 100644 --- a/engine/src/main/java/org/hibernate/validator/internal/util/logging/Log.java +++ b/engine/src/main/java/org/hibernate/validator/internal/util/logging/Log.java @@ -933,4 +933,7 @@ ConstraintDefinitionException getConstraintValidatorDefinitionConstraintMismatch @Message(id = 265, value = "Inconsistent show validation value in trace logs configuration. It is enabled via programmatic API, but explicitly disabled via properties.") ValidationException getInconsistentShowValidatedValuesInTraceLogsViolationConfigurationException(); + + @Message(id = 266, value = "'%c' is not a digit or upper case letter.") + IllegalArgumentException getCharacterIsNotADigitOrUpperCaseLetterException(char c); } diff --git a/engine/src/test/java/org/hibernate/validator/test/constraints/annotations/hv/br/CNPJValidatorTest.java b/engine/src/test/java/org/hibernate/validator/test/constraints/annotations/hv/br/CNPJValidatorTest.java index 8cdef0d08a..3c7fc69a16 100644 --- a/engine/src/test/java/org/hibernate/validator/test/constraints/annotations/hv/br/CNPJValidatorTest.java +++ b/engine/src/test/java/org/hibernate/validator/test/constraints/annotations/hv/br/CNPJValidatorTest.java @@ -35,6 +35,16 @@ public class CNPJValidatorTest extends AbstractConstrainedTest { "16.468.665/0001-64", "11.720.867/0001-38", "00.000.000/0001-91" }; + private String[] validCNPJsWithLetters = { + "70B0XZ010UTA84", "3Y59DJD8484J90", "84JNG734MJKD82", + "UU3UCXJCUDEM68", "ABCDEFGHIJKL80", "11AA22BB33CC06" + }; + + private String[] invalidCNPJsWithLetters = { + "70B0XZ010UTA83", "3Y59DJD8484J80", "84JNG734MJKD00", + "UU3UCXJCUDEM11", "ABCDEFGHIJKLAA", "11AA22BB33CC07" + }; + @Test @TestForIssue(jiraKey = "HV-1971") public void any_length_less_then_14_is_invalid() { @@ -127,6 +137,60 @@ public void correct_cnpj_with_check_digit_zero_validates() { assertNoViolations( violations ); } + @Test + @TestForIssue(jiraKey = "HV-1999") + public void incorrect_cnpj_with_letters_creates_constraint_violation() { + Set> violations = validator.validate( new Company( "9A50A90A000A66" ) ); + assertThat( violations ).containsOnlyViolations( + violationOf( CNPJ.class ).withProperty( "cnpj" ) + ); + } + + @Test + @TestForIssue(jiraKey = "HV-1999") + public void incorrect_cnpj_with_letters_separators_creates_constraint_violation() { + Set> violations = validator.validate( new Company( "9A.50A.90A/000A-66" ) ); + assertThat( violations ).containsOnlyViolations( + violationOf( CNPJ.class ).withProperty( "cnpj" ) + ); + } + + @Test + @TestForIssue(jiraKey = "HV-1999") + public void correct_new_cnpj_with_letters_validates() { + for ( String validCNPJ : validCNPJsWithLetters ) { + Set> violations = validator.validate( new CompanyCNPJWithLetters( validCNPJ ) ); + assertNoViolations( violations ); + } + } + + @Test + @TestForIssue(jiraKey = "HV-1999") + public void correct_new_cnpj_with_letters_separators_validates() { + Set> violations = validator.validate( new CompanyCNPJWithLetters( "9A.50A.90A/000A-66" ) ); + assertNoViolations( violations ); + } + + @Test + @TestForIssue(jiraKey = "HV-1999") + public void incorrect_new_cnpj_with_letters_creates_constraint_violation() { + Set> violations = validator.validate( new CompanyCNPJWithLetters( "9A.50A.90A/000A-67" ) ); + assertThat( violations ).containsOnlyViolations( + violationOf( CNPJ.class ).withProperty( "cnpj" ) + ); + } + + @Test + @TestForIssue(jiraKey = "HV-1999") + public void incorrect_new_cnpj_with_letters_separators_creates_constraint_violation() { + for ( String invalidCNPJ : invalidCNPJsWithLetters ) { + Set> violations = validator.validate( new CompanyCNPJWithLetters( invalidCNPJ ) ); + assertThat( violations ).containsOnlyViolations( + violationOf( CNPJ.class ).withProperty( "cnpj" ) + ); + } + } + public static class Company { @CNPJ private String cnpj; @@ -135,4 +199,13 @@ public Company(String cnpj) { this.cnpj = cnpj; } } + + public static class CompanyCNPJWithLetters { + @CNPJ(alphanumeric = true) + private String cnpj; + + public CompanyCNPJWithLetters(String cnpj) { + this.cnpj = cnpj; + } + } } diff --git a/engine/src/test/java/org/hibernate/validator/test/internal/constraintvalidators/hv/Mod11CheckValidatorTest.java b/engine/src/test/java/org/hibernate/validator/test/internal/constraintvalidators/hv/Mod11CheckValidatorTest.java index 496e91d99b..03634d5594 100644 --- a/engine/src/test/java/org/hibernate/validator/test/internal/constraintvalidators/hv/Mod11CheckValidatorTest.java +++ b/engine/src/test/java/org/hibernate/validator/test/internal/constraintvalidators/hv/Mod11CheckValidatorTest.java @@ -47,7 +47,8 @@ public void testInvalidStartIndex() { false, '0', '0', - ProcessingDirection.RIGHT_TO_LEFT + ProcessingDirection.RIGHT_TO_LEFT, + false ); validator.initialize( modCheck ); } @@ -62,7 +63,8 @@ public void testInvalidEndIndex() { false, '0', '0', - ProcessingDirection.RIGHT_TO_LEFT + ProcessingDirection.RIGHT_TO_LEFT, + false ); validator.initialize( modCheck ); } @@ -77,7 +79,8 @@ public void testEndIndexLessThanStartIndex() { false, '0', '0', - ProcessingDirection.RIGHT_TO_LEFT + ProcessingDirection.RIGHT_TO_LEFT, + false ); validator.initialize( modCheck ); } @@ -92,7 +95,8 @@ public void testInvalidcheckDigitIndex() { false, '0', '0', - ProcessingDirection.RIGHT_TO_LEFT + ProcessingDirection.RIGHT_TO_LEFT, + false ); validator.initialize( modCheck ); } @@ -107,7 +111,8 @@ public void testFailOnNonNumeric() throws Exception { false, '0', '0', - ProcessingDirection.RIGHT_TO_LEFT + ProcessingDirection.RIGHT_TO_LEFT, + false ); validator.initialize( modCheck ); @@ -124,7 +129,8 @@ public void testIgnoreNonNumeric() throws Exception { true, '0', '0', - ProcessingDirection.RIGHT_TO_LEFT + ProcessingDirection.RIGHT_TO_LEFT, + false ); validator.initialize( modCheck ); @@ -141,7 +147,8 @@ public void testIgnoreNonNumericWithCharCheckDigit() throws Exception { true, '0', 'X', - ProcessingDirection.LEFT_TO_RIGHT + ProcessingDirection.LEFT_TO_RIGHT, + false ); validator.initialize( modCheck ); @@ -158,7 +165,8 @@ public void testValidMod11() throws Exception { true, '0', '0', - ProcessingDirection.RIGHT_TO_LEFT + ProcessingDirection.RIGHT_TO_LEFT, + false ); validator.initialize( modCheck ); @@ -177,7 +185,8 @@ public void testInvalidMod11() throws Exception { true, '0', '0', - ProcessingDirection.RIGHT_TO_LEFT + ProcessingDirection.RIGHT_TO_LEFT, + false ); validator.initialize( modCheck ); @@ -196,7 +205,8 @@ public void testValidMod11CharCheckDigit() throws Exception { false, 'X', 'Z', - ProcessingDirection.RIGHT_TO_LEFT + ProcessingDirection.RIGHT_TO_LEFT, + false ); validator.initialize( modCheck ); @@ -214,7 +224,8 @@ public void testInvalidMod11CharCheckDigit() throws Exception { false, 'X', 'Z', - ProcessingDirection.RIGHT_TO_LEFT + ProcessingDirection.RIGHT_TO_LEFT, + false ); validator.initialize( modCheck ); @@ -232,7 +243,8 @@ public void testValidMod11ReverseOrder() throws Exception { false, 'X', 'Z', - ProcessingDirection.LEFT_TO_RIGHT + ProcessingDirection.LEFT_TO_RIGHT, + false ); validator.initialize( modCheck ); @@ -250,7 +262,8 @@ public void testInvalidMod11ReverseOrder() throws Exception { false, 'X', 'Z', - ProcessingDirection.LEFT_TO_RIGHT + ProcessingDirection.LEFT_TO_RIGHT, + false ); validator.initialize( modCheck ); @@ -284,6 +297,26 @@ public void testProgrammaticMod11Constraint() { assertNoViolations( constraintViolations ); } + + @Test + public void testValidMod11WithUpperLetters() throws Exception { + Mod11CheckValidator validator = new Mod11CheckValidator(); + Mod11Check modCheck = createMod11CheckAnnotation( + 0, + Integer.MAX_VALUE, + -1, + true, + '0', + '0', + ProcessingDirection.RIGHT_TO_LEFT, + true + ); + validator.initialize( modCheck ); + + assertTrue( validator.isValid( "9A50A90A000A0", null ) ); + assertTrue( validator.isValid( "9A.50A.90A/000A-0", null ) ); + } + private Mod11Check createMod11CheckAnnotation( int start, int end, @@ -291,7 +324,8 @@ private Mod11Check createMod11CheckAnnotation( boolean ignoreNonDigits, char treatCheck10As, char treatCheck11As, - ProcessingDirection processingDirection) { + ProcessingDirection processingDirection, + boolean alphanumeric) { ConstraintAnnotationDescriptor.Builder descriptorBuilder = new ConstraintAnnotationDescriptor.Builder<>( Mod11Check.class ); descriptorBuilder.setAttribute( "startIndex", start ); descriptorBuilder.setAttribute( "endIndex", end ); @@ -300,6 +334,7 @@ private Mod11Check createMod11CheckAnnotation( descriptorBuilder.setAttribute( "treatCheck10As", treatCheck10As ); descriptorBuilder.setAttribute( "treatCheck11As", treatCheck11As ); descriptorBuilder.setAttribute( "processingDirection", processingDirection ); + descriptorBuilder.setAttribute( "alphanumeric", alphanumeric ); return descriptorBuilder.build().getAnnotation(); }