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-1999 new CNPJ alphanumeric format #1365

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
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
Expand Up @@ -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;
Copy link
Member

Choose a reason for hiding this comment

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

I'd suggest limit the changes to CNPJ constraint only


/**
* Defines several {@code @Mod11Check} annotations on the same element.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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})")
Copy link
Member

Choose a reason for hiding this comment

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

Since this pattern now depends on the CNPJ's version/format, I'd suggest moving it into the CNPJ validator itself and picking the pattern that suits the format.
Doing so would also mean that @ReportAsSingleViolation can be removed.

@ReportAsSingleViolation
@Documented
@Constraint(validatedBy = { })
Expand All @@ -45,6 +45,8 @@

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

boolean alphanumeric() default false;
Copy link
Member

Choose a reason for hiding this comment

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

Wouldn't an enum here be more future-proof option? something like

Suggested change
boolean alphanumeric() default false;
FormatRevision formatRevision() default FormatRevision.V1;
enum FormatRevision {
    // Obviously, the constant names have to be more meaningful 
    //  and connected to the versions of current and new formats.
    V1, V2; 
}

This way, if another version is published, it can be added without any new parameters on the constraint annotation...


/**
* Defines several {@code @CNPJ} annotations on the same element.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ public void initialize(Mod11Check constraintAnnotation) {
constraintAnnotation.threshold(),
constraintAnnotation.treatCheck10As(),
constraintAnnotation.treatCheck11As(),
constraintAnnotation.processingDirection()
constraintAnnotation.processingDirection(),
constraintAnnotation.alphanumeric()
);
}

Expand All @@ -78,14 +79,16 @@ public void initialize(
char treatCheck10As,
char treatCheck11As,
ProcessingDirection direction,
boolean alphanumeric,
int... customWeights
) {

super.initialize(
startIndex,
endIndex,
checkDigitIndex,
ignoreNonDigitCharacters
ignoreNonDigitCharacters,
alphanumeric
);
this.threshold = threshold;
this.reverseOrder = direction == ProcessingDirection.LEFT_TO_RIGHT;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 ) {
Expand Down Expand Up @@ -79,11 +82,22 @@ public boolean isValid(final CharSequence value, final ConstraintValidatorContex

public abstract boolean isCheckDigitValid(List<Integer> 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();
}
Comment on lines +85 to +93
Copy link
Member

Choose a reason for hiding this comment

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

maybe just call

initialize(startIndex, endIndex, checkDigitIndex, ignoreNonValidCharacters, false)

and also if you'll end up still needing to modify the ModCheckBase consider using this overloaded version of initialize in places where alphanumeric is not needed so that less classes are modified.


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();
}
Expand All @@ -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;
Copy link
Member

Choose a reason for hiding this comment

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

This way of converting seems somewhat specific to CNPJ, I'd suggest creating an extended version of Mod11CheckValidator in CNPJValidator and overriding extractDigit in it.

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

Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
*/
public class CNPJValidator implements ConstraintValidator<CNPJ, CharSequence> {
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();
Expand All @@ -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()
);
}

Expand All @@ -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 );
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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<ConstraintViolation<Company>> 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<ConstraintViolation<Company>> 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<ConstraintViolation<CompanyCNPJWithLetters>> violations = validator.validate( new CompanyCNPJWithLetters( validCNPJ ) );
assertNoViolations( violations );
}
}

@Test
@TestForIssue(jiraKey = "HV-1999")
public void correct_new_cnpj_with_letters_separators_validates() {
Set<ConstraintViolation<CompanyCNPJWithLetters>> 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<ConstraintViolation<CompanyCNPJWithLetters>> 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<ConstraintViolation<CompanyCNPJWithLetters>> violations = validator.validate( new CompanyCNPJWithLetters( invalidCNPJ ) );
assertThat( violations ).containsOnlyViolations(
violationOf( CNPJ.class ).withProperty( "cnpj" )
);
}
}

public static class Company {
@CNPJ
private String cnpj;
Expand All @@ -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;
}
}
}
Loading