diff --git a/annotation-processor/src/main/java/org/hibernate/validator/ap/internal/util/ConstraintHelper.java b/annotation-processor/src/main/java/org/hibernate/validator/ap/internal/util/ConstraintHelper.java index c7b477ae94..d3ac8f2ce9 100644 --- a/annotation-processor/src/main/java/org/hibernate/validator/ap/internal/util/ConstraintHelper.java +++ b/annotation-processor/src/main/java/org/hibernate/validator/ap/internal/util/ConstraintHelper.java @@ -292,6 +292,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.IP_ADDRESS, CharSequence.class ); registerAllowedTypesForBuiltInConstraint( HibernateValidatorTypes.ISBN, CharSequence.class ); registerAllowedTypesForBuiltInConstraint( HibernateValidatorTypes.LENGTH, CharSequence.class ); registerAllowedTypesForBuiltInConstraint( HibernateValidatorTypes.MOD_CHECK, CharSequence.class ); diff --git a/annotation-processor/src/main/java/org/hibernate/validator/ap/internal/util/TypeNames.java b/annotation-processor/src/main/java/org/hibernate/validator/ap/internal/util/TypeNames.java index 30d7c2dd4c..7726ad383a 100644 --- a/annotation-processor/src/main/java/org/hibernate/validator/ap/internal/util/TypeNames.java +++ b/annotation-processor/src/main/java/org/hibernate/validator/ap/internal/util/TypeNames.java @@ -69,6 +69,7 @@ public static class HibernateValidatorTypes { public static final String CODE_POINT_LENGTH = ORG_HIBERNATE_VALIDATOR_CONSTRAINTS + ".CodePointLength"; public static final String CURRENCY = ORG_HIBERNATE_VALIDATOR_CONSTRAINTS + ".Currency"; public static final String EMAIL = ORG_HIBERNATE_VALIDATOR_CONSTRAINTS + ".Email"; + 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"; public static final String MOD_CHECK = ORG_HIBERNATE_VALIDATOR_CONSTRAINTS + ".ModCheck"; diff --git a/annotation-processor/src/test/java/org/hibernate/validator/ap/ConstraintValidationProcessorIT.java b/annotation-processor/src/test/java/org/hibernate/validator/ap/ConstraintValidationProcessorIT.java index e4d19b0804..678e9295be 100644 --- a/annotation-processor/src/test/java/org/hibernate/validator/ap/ConstraintValidationProcessorIT.java +++ b/annotation-processor/src/test/java/org/hibernate/validator/ap/ConstraintValidationProcessorIT.java @@ -22,6 +22,7 @@ import org.hibernate.validator.ap.testmodel.ModelWithCodePointLengthConstraints; import org.hibernate.validator.ap.testmodel.ModelWithDateConstraints; import org.hibernate.validator.ap.testmodel.ModelWithISBNConstraints; +import org.hibernate.validator.ap.testmodel.ModelWithIpAddressConstraints; import org.hibernate.validator.ap.testmodel.ModelWithJava8DateTime; import org.hibernate.validator.ap.testmodel.ModelWithJavaMoneyTypes; import org.hibernate.validator.ap.testmodel.ModelWithJodaTypes; @@ -810,4 +811,21 @@ public void bitcoinAddressConstraints() { new DiagnosticExpectation( Kind.ERROR, 20 ) ); } + + @Test + @TestForIssue(jiraKey = "HV-2137") + public void ipAddressConstraints() { + File[] sourceFiles = new File[] { + compilerHelper.getSourceFile( ModelWithIpAddressConstraints.class ) + }; + + boolean compilationResult = + compilerHelper.compile( new ConstraintValidationProcessor(), diagnostics, false, true, sourceFiles ); + + assertFalse( compilationResult ); + assertThatDiagnosticsMatch( + diagnostics, + new DiagnosticExpectation( Kind.ERROR, 20 ) + ); + } } diff --git a/annotation-processor/src/test/java/org/hibernate/validator/ap/testmodel/ModelWithIpAddressConstraints.java b/annotation-processor/src/test/java/org/hibernate/validator/ap/testmodel/ModelWithIpAddressConstraints.java new file mode 100644 index 0000000000..84476994fb --- /dev/null +++ b/annotation-processor/src/test/java/org/hibernate/validator/ap/testmodel/ModelWithIpAddressConstraints.java @@ -0,0 +1,22 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.validator.ap.testmodel; + +import org.hibernate.validator.constraints.IpAddress; + +/** + * @author Ivan Malutin + */ +public class ModelWithIpAddressConstraints { + + @IpAddress + private String string; + + @IpAddress + private CharSequence charSequence; + + @IpAddress + private Integer integer; +} diff --git a/documentation/src/main/asciidoc/_ch02.adoc b/documentation/src/main/asciidoc/_ch02.adoc index 1b2c3fe6df..8c76afca00 100644 --- a/documentation/src/main/asciidoc/_ch02.adoc +++ b/documentation/src/main/asciidoc/_ch02.adoc @@ -683,6 +683,11 @@ With one exception also these constraints apply to the field/property level, onl 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` + Hibernate metadata impact::: None + `@ISBN`:: Checks that the annotated character sequence is a valid https://en.wikipedia.org/wiki/International_Standard_Book_Number[ISBN]. `type` determines the type of ISBN. The default is ISBN-13. Supported data types::: `CharSequence` Hibernate metadata impact::: None diff --git a/engine/src/main/java/org/hibernate/validator/cfg/defs/IpAddressDef.java b/engine/src/main/java/org/hibernate/validator/cfg/defs/IpAddressDef.java new file mode 100644 index 0000000000..15db0d961e --- /dev/null +++ b/engine/src/main/java/org/hibernate/validator/cfg/defs/IpAddressDef.java @@ -0,0 +1,28 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.validator.cfg.defs; + +import org.hibernate.validator.Incubating; +import org.hibernate.validator.cfg.ConstraintDef; +import org.hibernate.validator.constraints.IpAddress; + +/** + * An {@link IpAddress} constraint definition. + * + * @author Ivan Malutin + * @since 9.1 + */ +@Incubating +public class IpAddressDef extends ConstraintDef { + + public IpAddressDef() { + super( IpAddress.class ); + } + + public IpAddressDef type(IpAddress.Type type) { + addParameter( "type", type ); + return this; + } +} diff --git a/engine/src/main/java/org/hibernate/validator/constraints/IpAddress.java b/engine/src/main/java/org/hibernate/validator/constraints/IpAddress.java new file mode 100644 index 0000000000..97c8e6e33f --- /dev/null +++ b/engine/src/main/java/org/hibernate/validator/constraints/IpAddress.java @@ -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.Retention; +import java.lang.annotation.Target; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import org.hibernate.validator.Incubating; + +/** + * Checks that the annotated character sequence is a valid + * IP address. + * The supported type is {@code CharSequence}. {@code null} is considered valid. + * + * @author Ivan Malutin + * @since 9.1 + */ +@Incubating +@Documented +@Constraint(validatedBy = { }) +@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) +@Retention(RUNTIME) +public @interface IpAddress { + + String message() default "{org.hibernate.validator.constraints.IpAddress.message}"; + + Class[] groups() default { }; + + Class[] payload() default { }; + + Type type() default Type.ANY; + + /** + * Defines the IP address version. + * Valid IP address versions are: + * + * When using {@code ANY}, an address is considered valid if it passes either + * IPv4 or IPv6 validation. + */ + enum Type { + IPv4, + IPv6, + ANY + } +} diff --git a/engine/src/main/java/org/hibernate/validator/internal/constraintvalidators/hv/IpAddressValidator.java b/engine/src/main/java/org/hibernate/validator/internal/constraintvalidators/hv/IpAddressValidator.java new file mode 100644 index 0000000000..94f3955f37 --- /dev/null +++ b/engine/src/main/java/org/hibernate/validator/internal/constraintvalidators/hv/IpAddressValidator.java @@ -0,0 +1,220 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.validator.internal.constraintvalidators.hv; + + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +import org.hibernate.validator.constraints.IpAddress; +import org.hibernate.validator.internal.util.Contracts; + +/** + * Checks that a given character sequence (e.g. string) is a valid IP address. + * + * @author Ivan Malutin + */ +public class IpAddressValidator implements ConstraintValidator { + + private IpAddressValidationAlgorithm ipAddressValidationAlgorithm; + + @Override + public void initialize(IpAddress constraintAnnotation) { + this.ipAddressValidationAlgorithm = IpAddressValidationAlgorithm.from( constraintAnnotation.type() ); + } + + @Override + public boolean isValid(CharSequence charSequence, ConstraintValidatorContext constraintValidatorContext) { + if ( charSequence == null ) { + return true; + } + + return ipAddressValidationAlgorithm.isValid( charSequence ); + } + + private enum IpAddressValidationAlgorithm { + IPv4 { + @Override + public boolean isValid(CharSequence ipAddress) { + return isValidIpV4( ipAddress, 0, ipAddress.length() ); + } + }, + IPv6 { + private static final int IPv4_SEGMENTS = 2; + private static final int IPv6_SEGMENTS_MAX_TOTAL = 8; + + @Override + public boolean isValid(CharSequence charSequence) { + if ( charSequence.length() < 2 ) { + // we at least need to have a :: + return false; + } + + // implementation is highly inspired by sun.net.util.IPAddressUtil + String ipAddress = charSequence.toString(); + // We ignore the scoped literal, so we look for % which defines it (scoped literal) if it's there: + int length = ipAddress.indexOf( '%' ); + if ( length < 0 ) { + length = ipAddress.length(); + } + + int i = 0; + int numberOfConsumedSegments = 0; + // Leading :: requires some special handling. + if ( ipAddress.charAt( i ) == ':' ) { + if ( ipAddress.charAt( ++i ) != ':' ) { + return false; + } + } + char currentCharacter; + boolean hasSuppression = false; + boolean previousConsumedTokenIsDigit = false; + int segmentLength = 0; + int val = 0; + int curtok = i; + + while ( i < length ) { + currentCharacter = ipAddress.charAt( i++ ); + + if ( currentCharacter == ':' ) { + curtok = i; + if ( !previousConsumedTokenIsDigit ) { + if ( hasSuppression ) { + return false; + } + hasSuppression = true; + continue; + } + else if ( i == length ) { + return false; + } + numberOfConsumedSegments++; + + if ( numberOfConsumedSegments > IPv6_SEGMENTS_MAX_TOTAL ) { + return false; + } + + previousConsumedTokenIsDigit = false; + val = 0; + segmentLength = 0; + continue; + } + // if we have a dot we assume it's an ipv4 segment. + // if that's so it can only be the last 32 bits of the IPv6 + // so we check we are looking at the last 2 segments (note we may have had some suppression + // and if that's so the number of consumed segments so far may be less than 6 + if ( currentCharacter == '.' ) { + if ( ( ( numberOfConsumedSegments + IPv4_SEGMENTS ) <= IPv6_SEGMENTS_MAX_TOTAL ) ) { + if ( !isValidIpV4( ipAddress, curtok, length ) ) { + return false; + } + } + else { + return false; + } + previousConsumedTokenIsDigit = false; + break; + } + int chval; + + char lowerCh = Character.toLowerCase( currentCharacter ); + if ( lowerCh >= 'a' && lowerCh <= 'f' ) { + chval = lowerCh - 'a' + 10; + } + else { + chval = currentCharacter - '0'; + + } + + if ( chval > -1 && chval < 17 ) { + val <<= 4; + val |= chval; + if ( val > 0xffff ) { + return false; + } + previousConsumedTokenIsDigit = true; + segmentLength++; + if ( segmentLength == 5 ) { + return false; + } + continue; + } + return false; + } + if ( previousConsumedTokenIsDigit ) { + if ( numberOfConsumedSegments + 1 > IPv6_SEGMENTS_MAX_TOTAL ) { + return false; + } + numberOfConsumedSegments++; + } + + if ( hasSuppression ) { + return numberOfConsumedSegments < IPv6_SEGMENTS_MAX_TOTAL; + } + return numberOfConsumedSegments == IPv6_SEGMENTS_MAX_TOTAL; + } + }, + ANY { + @Override + public boolean isValid(CharSequence charSequence) { + return IPv4.isValid( charSequence ) || IPv6.isValid( charSequence ); + } + }; + + abstract boolean isValid(CharSequence charSequence); + + static IpAddressValidationAlgorithm from(IpAddress.Type type) { + Contracts.assertNotNull( type ); + + return switch ( type ) { + case IPv4 -> IpAddressValidationAlgorithm.IPv4; + case IPv6 -> IpAddressValidationAlgorithm.IPv6; + case ANY -> IpAddressValidationAlgorithm.ANY; + }; + } + + static boolean isValidIpV4(CharSequence string, int start, int end) { + int length = end - start; + if ( length < 7 || length > 15 ) { + return false; + } + + // implementation inspired by sun.net.util.IPAddressUtil + int segmentValue = 0; + int segments = 1; + boolean newSegment = true; + + for ( int i = start; i < end; i++ ) { + char c = string.charAt( i ); + if ( c == '.' ) { + if ( newSegment || segmentValue < 0 || segmentValue > 255 || segments == 4 ) { + return false; + } + segments++; + segmentValue = 0; + newSegment = true; + } + else { + int digit = c - '0'; + if ( digit < 0 || digit > 9 ) { + return false; + } + segmentValue *= 10; + segmentValue += digit; + // to prevet any leading 0 in the segment + if ( !newSegment && segmentValue < 10 ) { + return false; + } + newSegment = false; + } + } + if ( newSegment || segmentValue < 0 || segmentValue > 255 || segments != 4 ) { + return false; + } + + return true; + } + } +} diff --git a/engine/src/main/java/org/hibernate/validator/internal/metadata/core/BuiltinConstraint.java b/engine/src/main/java/org/hibernate/validator/internal/metadata/core/BuiltinConstraint.java index e2b4b17128..a98f79e214 100644 --- a/engine/src/main/java/org/hibernate/validator/internal/metadata/core/BuiltinConstraint.java +++ b/engine/src/main/java/org/hibernate/validator/internal/metadata/core/BuiltinConstraint.java @@ -53,6 +53,7 @@ enum BuiltinConstraint { // Hibernate Validator specific constraints ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_CODE_POINT_LENGTH( "org.hibernate.validator.constraints.CodePointLength" ), ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_CURRENCY( "org.hibernate.validator.constraints.Currency" ), + 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" ), ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_LUHN_CHECK( "org.hibernate.validator.constraints.LuhnCheck" ), diff --git a/engine/src/main/java/org/hibernate/validator/internal/metadata/core/ConstraintHelper.java b/engine/src/main/java/org/hibernate/validator/internal/metadata/core/ConstraintHelper.java index 67c59f17f9..a24099f3f4 100644 --- a/engine/src/main/java/org/hibernate/validator/internal/metadata/core/ConstraintHelper.java +++ b/engine/src/main/java/org/hibernate/validator/internal/metadata/core/ConstraintHelper.java @@ -34,6 +34,7 @@ import static org.hibernate.validator.internal.metadata.core.BuiltinConstraint.ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_CREDIT_CARD_NUMBER; 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_EAN; +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; import static org.hibernate.validator.internal.metadata.core.BuiltinConstraint.ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_LENGTH; @@ -107,6 +108,7 @@ import org.hibernate.validator.constraints.Currency; import org.hibernate.validator.constraints.EAN; import org.hibernate.validator.constraints.ISBN; +import org.hibernate.validator.constraints.IpAddress; import org.hibernate.validator.constraints.Length; import org.hibernate.validator.constraints.LuhnCheck; import org.hibernate.validator.constraints.Mod10Check; @@ -327,6 +329,7 @@ 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.IpAddressValidator; import org.hibernate.validator.internal.constraintvalidators.hv.LengthValidator; import org.hibernate.validator.internal.constraintvalidators.hv.LuhnCheckValidator; import org.hibernate.validator.internal.constraintvalidators.hv.Mod10CheckValidator; @@ -754,6 +757,9 @@ protected Map, List> violations = validator.validate( foo ); + assertNoViolations( violations ); + } + + @Test + public void invalidIpAddress() { + Foo foo = new Foo( "256.256.256.256" ); + Set> violations = validator.validate( foo ); + assertThat( violations ).containsOnlyViolations( + violationOf( IpAddress.class ).withMessage( "invalid IP address" ) + ); + } + + private static class Foo { + @IpAddress + private final String ipAddress; + + public Foo(String ipAddress) { + this.ipAddress = ipAddress; + } + } +} diff --git a/engine/src/test/java/org/hibernate/validator/test/internal/constraintvalidators/hv/IpAddressValidatorTest.java b/engine/src/test/java/org/hibernate/validator/test/internal/constraintvalidators/hv/IpAddressValidatorTest.java new file mode 100644 index 0000000000..a2eecc78f3 --- /dev/null +++ b/engine/src/test/java/org/hibernate/validator/test/internal/constraintvalidators/hv/IpAddressValidatorTest.java @@ -0,0 +1,244 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.validator.test.internal.constraintvalidators.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 static org.hibernate.validator.testutils.ValidatorUtil.getConfiguration; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Stream; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validator; + +import org.hibernate.validator.HibernateValidator; +import org.hibernate.validator.HibernateValidatorConfiguration; +import org.hibernate.validator.cfg.ConstraintMapping; +import org.hibernate.validator.cfg.defs.IpAddressDef; +import org.hibernate.validator.constraints.IpAddress; +import org.hibernate.validator.internal.constraintvalidators.hv.IpAddressValidator; +import org.hibernate.validator.internal.util.annotation.ConstraintAnnotationDescriptor; +import org.hibernate.validator.testutil.TestForIssue; + +import org.testng.annotations.BeforeClass; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +/** + * Tests for {@link IpAddress} constraint validator ({@link IpAddress}). + * + * @author Ivan Malutin + */ +@TestForIssue(jiraKey = "HV-2137") +public class IpAddressValidatorTest { + + private IpAddressValidator validator; + + @BeforeClass + public void setUp() { + validator = new IpAddressValidator(); + } + + @Test(dataProvider = "validIPv4Addresses") + public void validIPv4Addresses(String ipAddress) { + validator.initialize( initializeAnnotation( IpAddress.Type.IPv4 ) ); + assertValidIpAddress( ipAddress ); + } + + @DataProvider(name = "validIPv4Addresses") + String[] validIPv4AddressesData() { + return new String[] { + "192.168.1.1", + "10.0.0.255", + "172.16.254.1", + "255.255.255.255", + "0.0.0.0", + "127.0.0.1", + "8.8.8.8", + "169.254.1.1", + "192.0.2.1", + "198.51.100.1" + }; + } + + + @Test(dataProvider = "invalidIPv4Addresses") + public void invalidIPv4Addresses(String ipAddress) { + validator.initialize( initializeAnnotation( IpAddress.Type.IPv4 ) ); + assertInvalidIpAddress( ipAddress ); + } + + @DataProvider(name = "invalidIPv4Addresses") + String[] invalidIPv4AddressesData() { + return new String[] { + "256.1.1.1", + "192.168.1", + "192.168.1.1.1", + "192.168.1.", + "192.0168.1.", + "192.168..1", + "192.168.1.01", + "qwe.qwe.qwe.qwe", + "192.168.1.1 ", + "192 .168.1.1", + "192.168.1.-1", + "192.168.1.", + "192.16.8.1.", + "19.2.16.8.1." + }; + } + + @Test(dataProvider = "validIPv6Addresses") + public void validIPv6Addresses(String ipAddress) { + validator.initialize( initializeAnnotation( IpAddress.Type.IPv6 ) ); + assertValidIpAddress( ipAddress ); + } + + @DataProvider(name = "validIPv6Addresses") + String[] validIPv6AddressesData() { + return new String[] { + "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + "2001:db8:85a3:0:0:8a2e:370:7334", + "2001:db8:85a3::8a2e:370:7334", + "2001:db8::", + "::ffff:192.168.1.1", + "ff02::1", + "2001:0:0:1::1", + "2001:db8:1234:5678:90ab:cdef:1234:5678", + "::ffff:c0a8:101", + "64:ff9b::192.168.1.1", + "2001:20::1", + "fc00::1", + "2001:db8:a::123", + "1:2:3:4:5:6:7:8", + "a:b:c:d:e:f:1:2", + "fe80::215:5dff:fe00:402", + "2001:db8:85a3:8d3:1319:8a2e:370:7348", + "::", + "::1", + "1::", + "::1234:5678", + "1:2:3:4:5:6:7::", + "2001:db8:85a3:8d3:1319:8a2e:370:7348%1234556", + "2001:db8:85a3:8d3:1319:8a2e:370:7348%eth0", + "2001:db8:85a3:8d3:1319:8a2e:370:7348%eth1", + "2001:db8:85a3:8d3:1319:8a2e:370:7348%eth2", + "2001:db8:85a3:8d3:1319:8a2e:370:7348%smth", + }; + } + + @Test(dataProvider = "invalidIPv6Addresses") + public void invalidIPv6Addresses(String ipAddress) { + validator.initialize( initializeAnnotation( IpAddress.Type.IPv6 ) ); + assertInvalidIpAddress( ipAddress ); + } + + @DataProvider(name = "invalidIPv6Addresses") + String[] invalidIPv6AddressesData() { + return new String[] { + "2001:db8:85a3:8d3:1319:8a2e:370", + "2001::85a3::8a2e", + "2001:db8:85a3:8d3:1319:8a2e:370:7334:", + ":2001:db8:85a3:8d3:1319:8a2e:370:7334", + "2001:db8:85a3:8d3:1319:8a2e:370:733g", + "2001:db8:85a3:8d3:1319:8a2e:370:73345", + "2001:db8:85a3:8d3:1319:8a2e:370:", + "2001:db8:::8a2e:370:7334", + "2001:db8:85a3:8d3:1319:8a2e:370:7334:abcd", + "2001:0db8:85a3:00000:0000:8a2e:0370:7334", + "02001:00db8:085a3:00000:00000:08a2e:00370:07334", + "::ffff:192.168.300.1", + "::ffff:192.168.1", + "::ffff:192.168.1.0.1", + "2001:db8:::192.1.1.1:370:7334", + "2001:db8:85a3:8d3:1319:8a2e:370:7334 :", + "2001:db8:85a3-8d3:1319:8a2e:370:7334", + "64:192.168.1.1::ff9b", + "2001::db8::1", + "ff02:::1", + "2001:db8:85a3:0:0:0:0:0:0", + ":1", + "2001:db8:85a3:8d3:1319:8a2e:370:", + "2001:db8:xyz::1", + ":::", + "1:2:3:4:5:6:7:8:9", + "1:2:3:4:5:6:7", + "1::2::3", + "1:2:3:4:5:6:7:8g", + "2001:0db8:85a3:0000:0000::8a2e:0370:7334", + "2001:0db8:85a3:0000:0000::8a2e:0370:0370:7334", + }; + } + + @Test(dataProvider = "testAnyValid") + public void testAnyValid(String ipAddress) { + validator.initialize( initializeAnnotation( IpAddress.Type.ANY ) ); + assertValidIpAddress( ipAddress ); + } + + @DataProvider(name = "testAnyValid") + String[] testAnyValidData() { + return Stream.concat( Arrays.stream( validIPv4AddressesData() ), Arrays.stream( validIPv6AddressesData() ) ) + .toArray( String[]::new ); + } + + @Test(dataProvider = "testAnyInvalid") + public void testAnyInvalid(String ipAddress) { + validator.initialize( initializeAnnotation( IpAddress.Type.ANY ) ); + assertInvalidIpAddress( ipAddress ); + } + + @DataProvider(name = "testAnyInvalid") + String[] testAnyInvalidData() { + return Stream.concat( Arrays.stream( invalidIPv4AddressesData() ), Arrays.stream( invalidIPv6AddressesData() ) ) + .toArray( String[]::new ); + } + + @Test + public void testIpAddressDef() { + HibernateValidatorConfiguration config = getConfiguration( HibernateValidator.class ); + ConstraintMapping mapping = config.createConstraintMapping(); + mapping.type( Foo.class ) + .field( "ipAddress" ) + .constraint( new IpAddressDef().type( IpAddress.Type.IPv4 ) ); + config.addMapping( mapping ); + Validator validator = config.buildValidatorFactory().getValidator(); + + Set> constraintViolations = validator.validate( new Foo( "127.0.0.1" ) ); + assertNoViolations( constraintViolations ); + + constraintViolations = validator.validate( new Foo( "256.256.256.256" ) ); + assertThat( constraintViolations ).containsOnlyViolations( + violationOf( IpAddress.class ) + ); + } + + private void assertValidIpAddress(String ipAddress) { + assertTrue( validator.isValid( ipAddress, null ), ipAddress + " should be a valid IP address" ); + } + + private void assertInvalidIpAddress(String ipAddress) { + assertFalse( validator.isValid( ipAddress, null ), ipAddress + " should be an invalid IP address" ); + } + + private IpAddress initializeAnnotation(IpAddress.Type type) { + ConstraintAnnotationDescriptor.Builder descriptorBuilder = new ConstraintAnnotationDescriptor.Builder<>( IpAddress.class ); + descriptorBuilder.setAttribute( "type", type ); + return descriptorBuilder.build().getAnnotation(); + } + + private static class Foo { + private final String ipAddress; + + public Foo(String ipAddress) { + this.ipAddress = ipAddress; + } + } +}