Skip to content
Open
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 @@ -294,6 +294,7 @@ public ConstraintHelper(Types typeUtils, AnnotationApiHelper annotationApiHelper
registerAllowedTypesForBuiltInConstraint( HibernateValidatorTypes.DURATION_MAX, Duration.class );
registerAllowedTypesForBuiltInConstraint( HibernateValidatorTypes.DURATION_MIN, Duration.class );
registerAllowedTypesForBuiltInConstraint( HibernateValidatorTypes.EMAIL, CharSequence.class );
registerAllowedTypesForBuiltInConstraint( HibernateValidatorTypes.IBAN, CharSequence.class );
registerAllowedTypesForBuiltInConstraint( HibernateValidatorTypes.IP_ADDRESS, CharSequence.class );
registerAllowedTypesForBuiltInConstraint( HibernateValidatorTypes.ISBN, CharSequence.class );
registerAllowedTypesForBuiltInConstraint( HibernateValidatorTypes.LENGTH, CharSequence.class );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ public static class HibernateValidatorTypes {
public static final String CURRENCY = ORG_HIBERNATE_VALIDATOR_CONSTRAINTS + ".Currency";
public static final String DATE_TIME_FORMAT = ORG_HIBERNATE_VALIDATOR_CONSTRAINTS + ".DateTimeFormat";
public static final String EMAIL = ORG_HIBERNATE_VALIDATOR_CONSTRAINTS + ".Email";
public static final String IBAN = ORG_HIBERNATE_VALIDATOR_CONSTRAINTS + ".IBAN";
public static final String IP_ADDRESS = ORG_HIBERNATE_VALIDATOR_CONSTRAINTS + ".IpAddress";
public static final String ISBN = ORG_HIBERNATE_VALIDATOR_CONSTRAINTS + ".ISBN";
public static final String LENGTH = ORG_HIBERNATE_VALIDATOR_CONSTRAINTS + ".Length";
Expand Down
4 changes: 4 additions & 0 deletions documentation/src/main/asciidoc/reference/_ch02.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,10 @@ With one exception also these constraints apply to the field/property level, onl
Supported data types::: `CharSequence`
Hibernate metadata impact::: None

`@IBAN`:: Checks that the annotated character sequence is a valid https://en.wikipedia.org/wiki/International_Bank_Account_Number[IBAN] (International Bank Account Number). The country-specific length and the ISO 7064 MOD 97-10 check digits are both verified. Spaces are ignored during validation.
Supported data types::: `CharSequence`
Hibernate metadata impact::: None

`@IpAddress`:: Checks that the annotated character sequence is a valid https://en.wikipedia.org/wiki/IP_address[IP address]. `type` determines the version of IP address.
The default is `ANY`, which means both IPv4 and IPv6 addresses are considered valid.
Supported data types::: `CharSequence`
Expand Down
21 changes: 21 additions & 0 deletions engine/src/main/java/org/hibernate/validator/cfg/defs/IBANDef.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.validator.cfg.defs;

import org.hibernate.validator.cfg.ConstraintDef;
import org.hibernate.validator.constraints.IBAN;

/**
* An {@link IBAN} constraint definition.
*
* @author Andrea Boriero
* @since 9.2
*/
public class IBANDef extends ConstraintDef<IBANDef, IBAN> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We usually add @Incubating to new constraints (so that we have more room to adjust one if needed)


public IBANDef() {
super( IBAN.class );
}
}
63 changes: 63 additions & 0 deletions engine/src/main/java/org/hibernate/validator/constraints/IBAN.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright Red Hat Inc. and Hibernate Authors
*/
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 jakarta.validation.Constraint;
import jakarta.validation.Payload;

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

/**
* Checks that the annotated character sequence is a valid
* <a href="https://en.wikipedia.org/wiki/International_Bank_Account_Number">IBAN</a>
* (International Bank Account Number).
* <p>
* The country-specific length and the ISO 7064 MOD 97-10 check digits are both verified.
* <p>
* The supported type is {@code CharSequence}. {@code null} is considered valid.
* <p>
* During validation spaces are ignored. This is useful when validating IBANs that use spaces
* to separate groups of characters (ex. {@code GB82 WEST 1234 5698 7654 32}).
*
* @author Andrea Boriero
* @since 9.2
*/
@Documented
@Constraint(validatedBy = { })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
public @interface IBAN {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

and same here about @Incubating


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

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

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Let's also have an attribute to disallow lowercase characters by default, but let the user flip the flag to allow them.

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

IBAN[] value();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.validator.internal.constraintvalidators.hv;

import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Pattern;

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

import org.hibernate.validator.constraints.IBAN;

/**
* Checks that a given character sequence (e.g. string) is a valid IBAN (International Bank Account Number).
* <p>
* Validation is performed by checking the country-specific length and the ISO 7064 MOD 97-10 check digits.
*
* @author Andrea Boriero
*/
public class IBANValidator implements ConstraintValidator<IBAN, CharSequence> {

private static final int MODULUS = 97;

/**
* The general IBAN structure: two letters (country code), two check digits and up to 30 alphanumeric
* characters (BBAN). The country-specific length is verified separately.
*/
private static final Pattern IBAN_STRUCTURE = Pattern.compile( "[A-Za-z]{2}[0-9]{2}[A-Za-z0-9]+" );

/**
* The expected total IBAN length per country, as defined by the SWIFT IBAN registry.
*/
private static final Map<String, Integer> IBAN_COUNTRY_LENGTHS = buildCountryLengths();

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

// Spaces are used to group characters when printing an IBAN, they are not part of the actual number.
final String iban = removeSpaces( value );

if ( !IBAN_STRUCTURE.matcher( iban ).matches() ) {
return false;
}

// Reject unknown country codes and any IBAN whose length does not match the fixed length defined for its country.
final Integer expectedLength = IBAN_COUNTRY_LENGTHS.get( iban.substring( 0, 2 ).toUpperCase( Locale.ROOT ) );
if ( expectedLength == null || expectedLength != iban.length() ) {
return false;
}

return hasValidCheckDigits( iban );
}

private static String removeSpaces(CharSequence value) {
return value.toString().replace( " ", "" );
}

/**
* Validates the ISO 7064 MOD 97-10 check digits: the first four characters are moved to the end,
* each letter is replaced by two digits ('A' = 10, ..., 'Z' = 35) and the resulting number must
* yield a remainder of 1 when divided by 97. The remainder is computed piece by piece to avoid
* building a potentially very large integer.
*/
private static boolean hasValidCheckDigits(String iban) {
int length = iban.length();
int mod = 0;
for ( int i = 0; i < length; i++ ) {
// Start with the BBAN (chars after the first four), then wrap around to the country code and check digits.
char c = iban.charAt( ( i + 4 ) % length );
if ( c >= '0' && c <= '9' ) {
mod = ( mod * 10 + ( c - '0' ) ) % MODULUS;
}
else {
mod = ( mod * 100 + ( Character.toUpperCase( c ) - 'A' + 10 ) ) % MODULUS;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I wonder if lowercase would even be valid ?

}
}
return mod == 1;
}

private static Map<String, Integer> buildCountryLengths() {
Map<String, Integer> lengths = new HashMap<>();
lengths.put( "AD", 24 );
lengths.put( "AE", 23 );
lengths.put( "AL", 28 );
lengths.put( "AT", 20 );
lengths.put( "AZ", 28 );
lengths.put( "BA", 20 );
lengths.put( "BE", 16 );
lengths.put( "BG", 22 );
lengths.put( "BH", 22 );
lengths.put( "BR", 29 );
lengths.put( "BY", 28 );
lengths.put( "CH", 21 );
lengths.put( "CR", 22 );
lengths.put( "CY", 28 );
lengths.put( "CZ", 24 );
lengths.put( "DE", 22 );
lengths.put( "DK", 18 );
lengths.put( "DO", 28 );
lengths.put( "EE", 20 );
lengths.put( "EG", 29 );
lengths.put( "ES", 24 );
lengths.put( "FI", 18 );
lengths.put( "FO", 18 );
lengths.put( "FR", 27 );
lengths.put( "GB", 22 );
lengths.put( "GE", 22 );
lengths.put( "GI", 23 );
lengths.put( "GL", 18 );
lengths.put( "GR", 27 );
lengths.put( "GT", 28 );
lengths.put( "HR", 21 );
lengths.put( "HU", 28 );
lengths.put( "IE", 22 );
lengths.put( "IL", 23 );
lengths.put( "IQ", 23 );
lengths.put( "IS", 26 );
lengths.put( "IT", 27 );
lengths.put( "JO", 30 );
lengths.put( "KW", 30 );
lengths.put( "KZ", 20 );
lengths.put( "LB", 28 );
lengths.put( "LC", 32 );
lengths.put( "LI", 21 );
lengths.put( "LT", 20 );
lengths.put( "LU", 20 );
lengths.put( "LV", 21 );
lengths.put( "LY", 25 );
lengths.put( "MC", 27 );
lengths.put( "MD", 24 );
lengths.put( "ME", 22 );
lengths.put( "MK", 19 );
lengths.put( "MR", 27 );
lengths.put( "MT", 31 );
lengths.put( "MU", 30 );
lengths.put( "NL", 18 );
lengths.put( "NO", 15 );
lengths.put( "PK", 24 );
lengths.put( "PL", 28 );
lengths.put( "PS", 29 );
lengths.put( "PT", 25 );
lengths.put( "QA", 29 );
lengths.put( "RO", 24 );
lengths.put( "RS", 22 );
lengths.put( "SA", 24 );
lengths.put( "SC", 31 );
lengths.put( "SD", 18 );
lengths.put( "SE", 24 );
lengths.put( "SI", 19 );
lengths.put( "SK", 24 );
lengths.put( "SM", 27 );
lengths.put( "ST", 25 );
lengths.put( "SV", 28 );
lengths.put( "TL", 23 );
lengths.put( "TN", 24 );
lengths.put( "TR", 26 );
lengths.put( "UA", 29 );
lengths.put( "VA", 22 );
lengths.put( "VG", 24 );
lengths.put( "XK", 20 );
return Collections.unmodifiableMap( lengths );
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ enum BuiltinConstraint {
ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_CONTAINS( "org.hibernate.validator.constraints.Contains" ),
ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_CURRENCY( "org.hibernate.validator.constraints.Currency" ),
ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_DATE_TIME_FORMAT( "org.hibernate.validator.constraints.DateTimeFormat" ),
ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_IBAN( "org.hibernate.validator.constraints.IBAN" ),
ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_IP_ADDRESS( "org.hibernate.validator.constraints.IpAddress" ),
ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_ISBN( "org.hibernate.validator.constraints.ISBN" ),
ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_LENGTH( "org.hibernate.validator.constraints.Length" ),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import static org.hibernate.validator.internal.metadata.core.BuiltinConstraint.ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_CURRENCY;
import static org.hibernate.validator.internal.metadata.core.BuiltinConstraint.ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_DATE_TIME_FORMAT;
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_IBAN;
import static org.hibernate.validator.internal.metadata.core.BuiltinConstraint.ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_IP_ADDRESS;
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;
Expand Down Expand Up @@ -117,6 +118,7 @@
import org.hibernate.validator.constraints.Currency;
import org.hibernate.validator.constraints.DateTimeFormat;
import org.hibernate.validator.constraints.EAN;
import org.hibernate.validator.constraints.IBAN;
import org.hibernate.validator.constraints.ISBN;
import org.hibernate.validator.constraints.IpAddress;
import org.hibernate.validator.constraints.Length;
Expand Down Expand Up @@ -346,6 +348,7 @@
import org.hibernate.validator.internal.constraintvalidators.hv.ContainsValidator;
import org.hibernate.validator.internal.constraintvalidators.hv.DateTimeFormatValidator;
import org.hibernate.validator.internal.constraintvalidators.hv.EANValidator;
import org.hibernate.validator.internal.constraintvalidators.hv.IBANValidator;
import org.hibernate.validator.internal.constraintvalidators.hv.ISBNValidator;
import org.hibernate.validator.internal.constraintvalidators.hv.IpAddressValidator;
import org.hibernate.validator.internal.constraintvalidators.hv.LengthValidator;
Expand Down Expand Up @@ -810,6 +813,9 @@ protected Map<Class<? extends Annotation>, List<? extends ConstraintValidatorDes
if ( enabledBuiltinConstraints.contains( ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_EAN ) ) {
putBuiltinConstraint( tmpConstraints, EAN.class, EANValidator.class );
}
if ( enabledBuiltinConstraints.contains( ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_IBAN ) ) {
putBuiltinConstraint( tmpConstraints, IBAN.class, IBANValidator.class );
}
if ( enabledBuiltinConstraints.contains( ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_IP_ADDRESS ) ) {
putBuiltinConstraint( tmpConstraints, IpAddress.class, IpAddressValidator.class );
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ jakarta.validation.constraints.Size.message = size must be between {m
org.hibernate.validator.constraints.CreditCardNumber.message = invalid credit card number
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.IBAN.message = invalid International Bank Account Number (IBAN)
org.hibernate.validator.constraints.IpAddress.message = invalid IP address
org.hibernate.validator.constraints.ISBN.message = invalid ISBN
org.hibernate.validator.constraints.Length.message = length must be between {min} and {max}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright Red Hat Inc. and Hibernate Authors
*/
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 jakarta.validation.ConstraintViolation;

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

import org.testng.annotations.Test;

/**
* Test to make sure that elements annotated with {@link IBAN} are validated.
*
* @author Andrea Boriero
*/
public class IBANConstrainedTest extends AbstractConstrainedTest {

@Test
public void testIBAN() {
Foo foo = new Foo( "GB82WEST12345698765432" );
Set<ConstraintViolation<Foo>> violations = validator.validate( foo );
assertNoViolations( violations );
}

@Test
public void testIBANInvalid() {
Foo foo = new Foo( "GB94WEST12345698765432" );
Set<ConstraintViolation<Foo>> violations = validator.validate( foo );
assertThat( violations ).containsOnlyViolations(
violationOf( IBAN.class ).withMessage( "invalid International Bank Account Number (IBAN)" )
);
}

private static class Foo {

@IBAN
private final String number;

public Foo(String number) {
this.number = number;
}
}
}
Loading
Loading