Skip to content
Merged
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;
}
5 changes: 5 additions & 0 deletions documentation/src/main/asciidoc/_ch02.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<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,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
* <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
*/
@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<? extends Payload>[] payload() default { };

Type type() default Type.ANY;

/**
* 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,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<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 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;
}
}
}
Loading