Skip to content

Commit

Permalink
HV-1541 Adding new ISBN constraint
Browse files Browse the repository at this point in the history
- Adding new ISBN constraint annotation.
- Adding new ConstraintValidator for ISBN constraint (ISBNValidator).
- Registering new constraint in ConstraintHelper so it is known to HV engine.
- Adding default message.
- Adding tests for isbn validation to make sure that validator logic is correct.
- Adding simple test with @ISBN to make sure that new constraint is recognised by engine and is validated.
  • Loading branch information
marko-bekhta authored and gsmet committed Dec 12, 2017
1 parent 6512e03 commit 5ba6668
Show file tree
Hide file tree
Showing 6 changed files with 341 additions and 0 deletions.
74 changes: 74 additions & 0 deletions engine/src/main/java/org/hibernate/validator/constraints/ISBN.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Hibernate Validator, declare and validate application constraints
*
* License: Apache License, Version 2.0
* See the license.txt file in the root directory or <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package org.hibernate.validator.constraints;

import 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 java.lang.annotation.Documented;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;

import org.hibernate.validator.constraints.ISBN.List;

/**
* Checks that the annotated character sequence is a valid
* <a href="https://en.wikipedia.org/wiki/International_Standard_Book_Number">ISBN</a>.
* The length of the number and the check digit are both verified.
* <p>
* The supported type is {@code CharSequence}. {@code null} is considered valid.
* <p>
* During validation all non ISBN characters are ignored. All digits and 'X' are considered
* to be valid ISBN characters. This is useful when validating ISBN with dashes separating
* parts of the number (ex. {@code 978-161-729-045-9}).
*
* @author Marko Bekhta
*/
@Documented
@Constraint(validatedBy = { })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
public @interface ISBN {

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

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

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

Type type() default Type.ISBN13;

/**
* Defines several {@code @ISBN} annotations on the same element.
*/
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
public @interface List {

ISBN[] value();
}

/**
* Defines the ISBN length. Valid lengths of ISBNs are {@code 10} and {@code 13}
* which are represented as {@link Type#ISBN10} and {@link Type#ISBN13} respectively.
*/
enum Type {
ISBN10,
ISBN13
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Hibernate Validator, declare and validate application constraints
*
* License: Apache License, Version 2.0
* See the license.txt file in the root directory or <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package org.hibernate.validator.internal.constraintvalidators.hv;

import java.util.function.Function;
import java.util.regex.Pattern;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

import org.hibernate.validator.constraints.ISBN;

/**
* Checks that a given character sequence (e.g. string) is a valid ISBN.
*
* @author Marko Bekhta
*/
public class ISBNValidator implements ConstraintValidator<ISBN, CharSequence> {

/**
* Pattern to replace all non ISBN characters. ISBN can have digits or 'X'.
*/
private static Pattern NOT_DIGITS_OR_NOT_X = Pattern.compile( "[^\\dX]" );

private int length;

private Function<String, Boolean> checkChecksumFunction;

@Override
public void initialize(ISBN constraintAnnotation) {
switch ( constraintAnnotation.type() ) {
case ISBN10:
length = 10;
checkChecksumFunction = this::checkChecksumISBN10;
break;
case ISBN13:
length = 13;
checkChecksumFunction = this::checkChecksumISBN13;
break;
}
}

@Override
public boolean isValid(CharSequence isbn, ConstraintValidatorContext context) {
if ( isbn == null ) {
return true;
}

// Replace all non-digit (or !=X) chars
String digits = NOT_DIGITS_OR_NOT_X.matcher( isbn ).replaceAll( "" );

// Check if the length of resulting string matches the expecting one
if ( digits.length() != length ) {
return false;
}

return checkChecksumFunction.apply( digits );
}

/**
* Check the digits for ISBN 10 using algorithm from
* <a href="https://en.wikipedia.org/wiki/International_Standard_Book_Number#ISBN-10_check_digits">Wikipedia</a>.
*/
private boolean checkChecksumISBN10(String isbn) {
int sum = 0;
for ( int i = 0; i < isbn.length() - 1; i++ ) {
sum += ( isbn.charAt( i ) - '0' ) * ( i + 1 );
}
char checkSum = isbn.charAt( 9 );
return sum % 11 == ( checkSum == 'X' ? 10 : checkSum - '0' );
}

/**
* Check the digits for ISBN 13 using algorithm from
* <a href="https://en.wikipedia.org/wiki/International_Standard_Book_Number#ISBN-13_check_digit_calculation">Wikipedia</a>.
*/
private boolean checkChecksumISBN13(String isbn) {
int sum = 0;
for ( int i = 0; i < isbn.length() - 1; i++ ) {
sum += ( isbn.charAt( i ) - '0' ) * ( i % 2 == 0 ? 1 : 3 );
}
char checkSum = isbn.charAt( 12 );
return 10 - sum % 10 == ( checkSum - '0' );
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
import org.hibernate.validator.constraints.ConstraintComposition;
import org.hibernate.validator.constraints.Currency;
import org.hibernate.validator.constraints.EAN;
import org.hibernate.validator.constraints.ISBN;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.LuhnCheck;
import org.hibernate.validator.constraints.Mod10Check;
Expand Down Expand Up @@ -254,6 +255,7 @@
import org.hibernate.validator.internal.constraintvalidators.bv.time.pastorpresent.PastOrPresentValidatorForZonedDateTime;
import org.hibernate.validator.internal.constraintvalidators.hv.CodePointLengthValidator;
import org.hibernate.validator.internal.constraintvalidators.hv.EANValidator;
import org.hibernate.validator.internal.constraintvalidators.hv.ISBNValidator;
import org.hibernate.validator.internal.constraintvalidators.hv.LengthValidator;
import org.hibernate.validator.internal.constraintvalidators.hv.LuhnCheckValidator;
import org.hibernate.validator.internal.constraintvalidators.hv.Mod10CheckValidator;
Expand Down Expand Up @@ -415,6 +417,8 @@ public ConstraintHelper() {

putConstraints( tmpConstraints, FutureOrPresent.class, futureOrPresentValidators );

putConstraint( tmpConstraints, ISBN.class, ISBNValidator.class );

if ( isJavaMoneyInClasspath() ) {
putConstraints( tmpConstraints, Max.class, Arrays.asList(
MaxValidatorForBigDecimal.class,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ org.hibernate.validator.constraints.CreditCardNumber.message = invalid cr
org.hibernate.validator.constraints.Currency.message = invalid currency (must be one of {value})
org.hibernate.validator.constraints.EAN.message = invalid {type} barcode
org.hibernate.validator.constraints.Email.message = not a well-formed email address
org.hibernate.validator.constraints.ISBN.message = invalid ISBN
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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* 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;

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.util.Set;

import javax.validation.ConstraintViolation;

import org.hibernate.validator.constraints.ISBN;
import org.hibernate.validator.test.constraints.annotations.AbstractConstrainedTest;

import org.testng.annotations.Test;

/**
* Test to make sure that elements annotated with {@link ISBN} are validated.
*
* @author Marko Bekhta
*/
public class ISBNConstrainedTest extends AbstractConstrainedTest {

@Test
public void testISBN() {
Foo foo = new Foo( "978-1-56619-909-4" );
Set<ConstraintViolation<Foo>> violations = validator.validate( foo );
assertNoViolations( violations );
}

@Test
public void testISBNInvalid() {
Foo foo = new Foo( "5412-3456-7890" );
Set<ConstraintViolation<Foo>> violations = validator.validate( foo );
assertThat( violations ).containsOnlyViolations(
violationOf( ISBN.class ).withMessage( "invalid ISBN" )
);
}

private static class Foo {

@ISBN
private final String number;

public Foo(String number) {
this.number = number;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* Hibernate Validator, declare and validate application constraints
*
* License: Apache License, Version 2.0
* See the license.txt file in the root directory or <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package org.hibernate.validator.test.internal.constraintvalidators.hv;

import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;

import org.hibernate.validator.constraints.ISBN;
import org.hibernate.validator.internal.constraintvalidators.hv.ISBNValidator;
import org.hibernate.validator.internal.util.annotation.ConstraintAnnotationDescriptor;

import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

/**
* A set of tests for {@link ISBN} constraint validator ({@link ISBNValidator}), which
* make sure that validation is performed correctly.
*
* @author Marko Bekhta
*/
public class ISBNValidatorTest {

private ISBNValidator validator;

@BeforeMethod
public void setUp() throws Exception {
validator = new ISBNValidator();
}

@Test
public void validISBN10() throws Exception {
validator.initialize( initializeAnnotation( ISBN.Type.ISBN10 ) );

assertValidISBN( null );
assertValidISBN( "99921-58-10-7" );
assertValidISBN( "9971-5-0210-0" );
assertValidISBN( "960-425-059-0" );
assertValidISBN( "80-902734-1-6" );
assertValidISBN( "0-9752298-0-X" );
assertValidISBN( "0-85131-041-9" );
assertValidISBN( "0-684-84328-5" );
assertValidISBN( "1-84356-028-3" );
}

@Test
public void invalidISBN10() throws Exception {
validator.initialize( initializeAnnotation( ISBN.Type.ISBN10 ) );

// invalid check-digit
assertInvalidISBN( "99921-58-10-8" );
assertInvalidISBN( "9971-5-0210-1" );
assertInvalidISBN( "960-425-059-2" );
assertInvalidISBN( "80-902734-1-8" );
assertInvalidISBN( "0-9752298-0-3" );
assertInvalidISBN( "0-85131-041-X" );
assertInvalidISBN( "0-684-84328-7" );
assertInvalidISBN( "1-84356-028-1" );

// invalid length
assertInvalidISBN( "" );
assertInvalidISBN( "978-0-5" );
assertInvalidISBN( "978-0-55555555555555" );
}

@Test
public void validISBN13() throws Exception {
validator.initialize( initializeAnnotation( ISBN.Type.ISBN13 ) );

assertValidISBN( null );
assertValidISBN( "978-123-456-789-7" );
assertValidISBN( "978-91-983989-1-5" );
assertValidISBN( "978-988-785-411-1" );
assertValidISBN( "978-1-56619-909-4" );
assertValidISBN( "978-1-4028-9462-6" );
assertValidISBN( "978-0-85131-041-1" );
assertValidISBN( "978-0-684-84328-5" );
assertValidISBN( "978-1-84356-028-9" );
assertValidISBN( "978-0-54560-495-6" );
}

@Test
public void invalidISBN13() throws Exception {
validator.initialize( initializeAnnotation( ISBN.Type.ISBN13 ) );

// invalid check-digit
assertInvalidISBN( "978-123-456-789-6" );
assertInvalidISBN( "978-91-983989-1-4" );
assertInvalidISBN( "978-988-785-411-2" );
assertInvalidISBN( "978-1-56619-909-1" );
assertInvalidISBN( "978-1-4028-9462-0" );
assertInvalidISBN( "978-0-85131-041-5" );
assertInvalidISBN( "978-0-684-84328-1" );
assertInvalidISBN( "978-1-84356-028-1" );
assertInvalidISBN( "978-0-54560-495-4" );

// invalid length
assertInvalidISBN( "" );
assertInvalidISBN( "978-0-54560-4" );
assertInvalidISBN( "978-0-55555555555555" );
}

private void assertValidISBN(String isbn) {
assertTrue( validator.isValid( isbn, null ), isbn + " should be a valid ISBN" );
}

private void assertInvalidISBN(String isbn) {
assertFalse( validator.isValid( isbn, null ), isbn + " should be an invalid ISBN" );
}

private ISBN initializeAnnotation(ISBN.Type type) {
ConstraintAnnotationDescriptor.Builder<ISBN> descriptorBuilder = new ConstraintAnnotationDescriptor.Builder<>( ISBN.class );
descriptorBuilder.setAttribute( "type", type );
return descriptorBuilder.build().getAnnotation();
}
}

0 comments on commit 5ba6668

Please sign in to comment.