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 @@ -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 );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 )
);
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
4 changes: 4 additions & 0 deletions documentation/src/main/asciidoc/ch02.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,10 @@ 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 IPv4.
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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* 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.IpAddress;

/**
* An {@link IpAddress} constraint definition.
*
* @author Ivan Malutin
* @since 9.1
*/
public class IpAddressDef extends ConstraintDef<IpAddressDef, IpAddress> {

public IpAddressDef() {
super(IpAddress.class);
}

public IpAddressDef type(IpAddress.Type type) {
addParameter("type", type);
return this;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* 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;

/**
* Checks that the annotated character sequence is a valid
* <a href="https://en.wikipedia.org/wiki/IP_address">IP address</a>.
* The supported type is {@code CharSequence}. {@code null} is considered valid.
*
* @author Ivan Malutin
* @since 9.1
*/
@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<? extends Payload>[] payload() default { };

Type type() default Type.IPv4;

/**
* Defines the IP address version.
* Valid IP address versions are:
* <ul>
* <li>{@code IPv4} - for IPv4 addresses (version 4)</li>
* <li>{@code IPv6} - for IPv6 addresses (version 6)</li>
* <li>{@code ANY} - for validating IP addresses that could be either IPv4 or IPv6</li>
* </ul>
* When using {@code ANY}, an address is considered valid if it passes either
* IPv4 or IPv6 validation.
*/
enum Type {
IPv4,
IPv6,
ANY
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.validator.internal.constraintvalidators.hv;


import java.util.Arrays;
import java.util.List;

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<IpAddress, CharSequence> {

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 interface IpAddressValidationAlgorithm {
boolean isValid(CharSequence charSequence);

static IpAddressValidationAlgorithm from(IpAddress.Type type) {
Contracts.assertNotNull( type );

return switch ( type ) {
case IPv4 -> IpAddressValidationAlgorithmImpl.IPv4;
case IPv6 -> IpAddressValidationAlgorithmImpl.IPv6;
case ANY -> IpAddressValidationAlgorithmImpl.ANY;
};
}
}

private enum IpAddressValidationAlgorithmImpl implements IpAddressValidationAlgorithm {
IPv4 {
@Override
public boolean isValid(CharSequence charSequence) {
String ipAddress = charSequence.toString();

String[] parts = ipAddress.split( "\\." );

if ( parts.length != 4 ) {
return false;
}

for ( String part : parts ) {
if ( part.isBlank() || ( part.length() > 1 && part.startsWith( "0" ) ) ) {
return false;
}

int num;
try {
num = Integer.parseInt( part );
}
catch (NumberFormatException e) {
return false;
}
if ( num < 0 || 255 < num ) {
return false;
}

}

return true;
}
},
IPv6 {
@Override
public boolean isValid(CharSequence charSequence) {
String ipAddress = charSequence.toString();

int compressionIndex = ipAddress.indexOf( "::" );
if ( compressionIndex != -1 && ipAddress.lastIndexOf( "::" ) != compressionIndex ) {
return false;
}

boolean startsWithCompression = ipAddress.startsWith( "::" );
boolean endsWithCompression = ipAddress.endsWith( "::" );
if ( ( ipAddress.startsWith( ":" ) && !startsWithCompression ) || ( ipAddress.endsWith( ":" ) && !endsWithCompression ) ) {
return false;
}

String[] parts = ipAddress.split( ":" );
boolean hasCompression = compressionIndex != -1;

if ( hasCompression ) {
List<String> partsList = Arrays.asList( parts );
if ( endsWithCompression ) {
partsList.add( "" );
}
else if ( startsWithCompression && !partsList.isEmpty() ) {
partsList.remove( 0 );
}
parts = partsList.toArray( new String[0] );
}

if ( parts.length > 8 ) {
return false;
}

int partsCount = 0;
for ( int i = 0; i < parts.length; i++ ) {
String part = parts[i];

if ( part.isBlank() ) {
if ( i > 0 && parts[i - 1].isBlank() ) {
return false;
}
}
else if ( i == parts.length - 1 && part.contains( "." ) ) {
if ( !IPv4.isValid( part ) ) {
return false;
}
partsCount += 2;
}
else {
if ( part.length() > 4 ) {
return false;
}
int num;
try {
num = Integer.parseInt( part, 16 );
}
catch (NumberFormatException e) {
return false;
}
if ( num < 0 || num > 0xFFFF ) {
return false;
}
partsCount++;
}
}

if ( partsCount > 8 || ( partsCount < 8 && !hasCompression ) ) {
return false;
}

return true;
}
},
ANY {
@Override
public boolean isValid(CharSequence charSequence) {
return IPv4.isValid( charSequence ) || IPv6.isValid( charSequence );
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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" ),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -754,6 +757,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_IP_ADDRESS ) ) {
putBuiltinConstraint( tmpConstraints, IpAddress.class, IpAddressValidator.class );
}
if ( enabledBuiltinConstraints.contains( ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_ISBN ) ) {
putBuiltinConstraint( tmpConstraints, ISBN.class, ISBNValidator.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.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}
org.hibernate.validator.constraints.CodePointLength.message = length must be between {min} and {max}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ jakarta.validation.constraints.Size.message = \u0440\u0430\u0437\u043
org.hibernate.validator.constraints.CreditCardNumber.message = \u043d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0439 \u043d\u043e\u043c\u0435\u0440 \u043a\u0440\u0435\u0434\u0438\u0442\u043d\u043e\u0439 \u043a\u0430\u0440\u0442\u044b
org.hibernate.validator.constraints.Currency.message = \u043d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u0430\u044f \u0434\u0435\u043d\u0435\u0436\u043d\u0430\u044f \u0435\u0434\u0438\u043d\u0438\u0446\u0430 (\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u044b\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f: {value})
org.hibernate.validator.constraints.EAN.message = \u043d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0439 \u0448\u0442\u0440\u0438\u0445\u043e\u0432\u043e\u0439 \u043a\u043e\u0434 {type}
org.hibernate.validator.constraints.IpAddress.message = \u043d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0439\u0020\u0049\u0050\u0020\u0430\u0434\u0440\u0435\u0441
org.hibernate.validator.constraints.ISBN.message = \u043d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0439 ISBN
org.hibernate.validator.constraints.Length.message = \u0434\u043b\u0438\u043d\u0430 \u0434\u043e\u043b\u0436\u043d\u0430 \u0441\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0442\u044c \u043e\u0442 {min} \u0434\u043e {max}
org.hibernate.validator.constraints.CodePointLength.message = \u0434\u043b\u0438\u043d\u0430 \u0434\u043e\u043b\u0436\u043d\u0430 \u0441\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0442\u044c \u043e\u0442 {min} \u0434\u043e {max}
Expand Down
Loading