Skip to content

Commit 7605fe6

Browse files
committed
HV-2137 Adding new IpAddress constraint
1 parent 6689d2d commit 7605fe6

File tree

14 files changed

+565
-0
lines changed

14 files changed

+565
-0
lines changed

annotation-processor/src/main/java/org/hibernate/validator/ap/internal/util/ConstraintHelper.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,7 @@ public ConstraintHelper(Types typeUtils, AnnotationApiHelper annotationApiHelper
292292
registerAllowedTypesForBuiltInConstraint( HibernateValidatorTypes.DURATION_MAX, Duration.class );
293293
registerAllowedTypesForBuiltInConstraint( HibernateValidatorTypes.DURATION_MIN, Duration.class );
294294
registerAllowedTypesForBuiltInConstraint( HibernateValidatorTypes.EMAIL, CharSequence.class );
295+
registerAllowedTypesForBuiltInConstraint( HibernateValidatorTypes.IP_ADDRESS, CharSequence.class );
295296
registerAllowedTypesForBuiltInConstraint( HibernateValidatorTypes.ISBN, CharSequence.class );
296297
registerAllowedTypesForBuiltInConstraint( HibernateValidatorTypes.LENGTH, CharSequence.class );
297298
registerAllowedTypesForBuiltInConstraint( HibernateValidatorTypes.MOD_CHECK, CharSequence.class );

annotation-processor/src/main/java/org/hibernate/validator/ap/internal/util/TypeNames.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ public static class HibernateValidatorTypes {
6969
public static final String CODE_POINT_LENGTH = ORG_HIBERNATE_VALIDATOR_CONSTRAINTS + ".CodePointLength";
7070
public static final String CURRENCY = ORG_HIBERNATE_VALIDATOR_CONSTRAINTS + ".Currency";
7171
public static final String EMAIL = ORG_HIBERNATE_VALIDATOR_CONSTRAINTS + ".Email";
72+
public static final String IP_ADDRESS = ORG_HIBERNATE_VALIDATOR_CONSTRAINTS + ".IpAddress";
7273
public static final String ISBN = ORG_HIBERNATE_VALIDATOR_CONSTRAINTS + ".ISBN";
7374
public static final String LENGTH = ORG_HIBERNATE_VALIDATOR_CONSTRAINTS + ".Length";
7475
public static final String MOD_CHECK = ORG_HIBERNATE_VALIDATOR_CONSTRAINTS + ".ModCheck";

annotation-processor/src/test/java/org/hibernate/validator/ap/ConstraintValidationProcessorIT.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.hibernate.validator.ap.testmodel.ModelWithCodePointLengthConstraints;
2323
import org.hibernate.validator.ap.testmodel.ModelWithDateConstraints;
2424
import org.hibernate.validator.ap.testmodel.ModelWithISBNConstraints;
25+
import org.hibernate.validator.ap.testmodel.ModelWithIpAddressConstraints;
2526
import org.hibernate.validator.ap.testmodel.ModelWithJava8DateTime;
2627
import org.hibernate.validator.ap.testmodel.ModelWithJavaMoneyTypes;
2728
import org.hibernate.validator.ap.testmodel.ModelWithJodaTypes;
@@ -810,4 +811,21 @@ public void bitcoinAddressConstraints() {
810811
new DiagnosticExpectation( Kind.ERROR, 20 )
811812
);
812813
}
814+
815+
@Test
816+
@TestForIssue(jiraKey = "HV-2137")
817+
public void ipAddressConstraints() {
818+
File[] sourceFiles = new File[] {
819+
compilerHelper.getSourceFile( ModelWithIpAddressConstraints.class )
820+
};
821+
822+
boolean compilationResult =
823+
compilerHelper.compile( new ConstraintValidationProcessor(), diagnostics, false, true, sourceFiles );
824+
825+
assertFalse( compilationResult );
826+
assertThatDiagnosticsMatch(
827+
diagnostics,
828+
new DiagnosticExpectation( Kind.ERROR, 20 )
829+
);
830+
}
813831
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.validator.ap.testmodel;
6+
7+
import org.hibernate.validator.constraints.IpAddress;
8+
9+
/**
10+
* @author Ivan Malutin
11+
*/
12+
public class ModelWithIpAddressConstraints {
13+
14+
@IpAddress
15+
private String string;
16+
17+
@IpAddress
18+
private CharSequence charSequence;
19+
20+
@IpAddress
21+
private Integer integer;
22+
}

documentation/src/main/asciidoc/ch02.asciidoc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,10 @@ With one exception also these constraints apply to the field/property level, onl
683683
Supported data types::: `CharSequence`
684684
Hibernate metadata impact::: None
685685

686+
`@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.
687+
Supported data types::: `CharSequence`
688+
Hibernate metadata impact::: None
689+
686690
`@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.
687691
Supported data types::: `CharSequence`
688692
Hibernate metadata impact::: None
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.validator.cfg.defs;
6+
7+
import org.hibernate.validator.cfg.ConstraintDef;
8+
import org.hibernate.validator.constraints.IpAddress;
9+
10+
/**
11+
* An {@link IpAddress} constraint definition.
12+
*
13+
* @author Ivan Malutin
14+
* @since 9.1
15+
*/
16+
public class IpAddressDef extends ConstraintDef<IpAddressDef, IpAddress> {
17+
18+
public IpAddressDef() {
19+
super(IpAddress.class);
20+
}
21+
22+
public IpAddressDef type(IpAddress.Type type) {
23+
addParameter("type", type);
24+
return this;
25+
}
26+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.validator.constraints;
6+
7+
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
8+
import static java.lang.annotation.ElementType.CONSTRUCTOR;
9+
import static java.lang.annotation.ElementType.FIELD;
10+
import static java.lang.annotation.ElementType.METHOD;
11+
import static java.lang.annotation.ElementType.PARAMETER;
12+
import static java.lang.annotation.ElementType.TYPE_USE;
13+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
14+
15+
import java.lang.annotation.Documented;
16+
import java.lang.annotation.Retention;
17+
import java.lang.annotation.Target;
18+
19+
import jakarta.validation.Constraint;
20+
import jakarta.validation.Payload;
21+
22+
/**
23+
* Checks that the annotated character sequence is a valid
24+
* <a href="https://en.wikipedia.org/wiki/IP_address">IP address</a>.
25+
* The supported type is {@code CharSequence}. {@code null} is considered valid.
26+
*
27+
* @author Ivan Malutin
28+
* @since 9.1
29+
*/
30+
@Documented
31+
@Constraint(validatedBy = { })
32+
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
33+
@Retention(RUNTIME)
34+
public @interface IpAddress {
35+
36+
String message() default "{org.hibernate.validator.constraints.IpAddress.message}";
37+
38+
Class<?>[] groups() default { };
39+
40+
Class<? extends Payload>[] payload() default { };
41+
42+
Type type() default Type.IPv4;
43+
44+
/**
45+
* Defines the IP address version.
46+
* Valid IP address versions are:
47+
* <ul>
48+
* <li>{@code IPv4} - for IPv4 addresses (version 4)</li>
49+
* <li>{@code IPv6} - for IPv6 addresses (version 6)</li>
50+
* <li>{@code ANY} - for validating IP addresses that could be either IPv4 or IPv6</li>
51+
* </ul>
52+
* When using {@code ANY}, an address is considered valid if it passes either
53+
* IPv4 or IPv6 validation.
54+
*/
55+
enum Type {
56+
IPv4,
57+
IPv6,
58+
ANY
59+
}
60+
}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.validator.internal.constraintvalidators.hv;
6+
7+
8+
import java.util.Arrays;
9+
import java.util.List;
10+
11+
import jakarta.validation.ConstraintValidator;
12+
import jakarta.validation.ConstraintValidatorContext;
13+
14+
import org.hibernate.validator.constraints.IpAddress;
15+
import org.hibernate.validator.internal.util.Contracts;
16+
17+
/**
18+
* Checks that a given character sequence (e.g. string) is a valid IP address.
19+
*
20+
* @author Ivan Malutin
21+
*/
22+
public class IpAddressValidator implements ConstraintValidator<IpAddress, CharSequence> {
23+
24+
private IpAddressValidationAlgorithm ipAddressValidationAlgorithm;
25+
26+
@Override
27+
public void initialize(IpAddress constraintAnnotation) {
28+
this.ipAddressValidationAlgorithm = IpAddressValidationAlgorithm.from( constraintAnnotation.type() );
29+
}
30+
31+
@Override
32+
public boolean isValid(CharSequence charSequence, ConstraintValidatorContext constraintValidatorContext) {
33+
if ( charSequence == null ) {
34+
return true;
35+
}
36+
37+
return ipAddressValidationAlgorithm.isValid( charSequence );
38+
}
39+
40+
private interface IpAddressValidationAlgorithm {
41+
boolean isValid(CharSequence charSequence);
42+
43+
static IpAddressValidationAlgorithm from(IpAddress.Type type) {
44+
Contracts.assertNotNull( type );
45+
46+
return switch ( type ) {
47+
case IPv4 -> IpAddressValidationAlgorithmImpl.IPv4;
48+
case IPv6 -> IpAddressValidationAlgorithmImpl.IPv6;
49+
case ANY -> IpAddressValidationAlgorithmImpl.ANY;
50+
};
51+
}
52+
}
53+
54+
private enum IpAddressValidationAlgorithmImpl implements IpAddressValidationAlgorithm {
55+
IPv4 {
56+
@Override
57+
public boolean isValid(CharSequence charSequence) {
58+
String ipAddress = charSequence.toString();
59+
60+
String[] parts = ipAddress.split( "\\." );
61+
62+
if ( parts.length != 4 ) {
63+
return false;
64+
}
65+
66+
for ( String part : parts ) {
67+
if ( part.isBlank() || ( part.length() > 1 && part.startsWith( "0" ) ) ) {
68+
return false;
69+
}
70+
71+
int num;
72+
try {
73+
num = Integer.parseInt( part );
74+
}
75+
catch (NumberFormatException e) {
76+
return false;
77+
}
78+
if ( num < 0 || 255 < num ) {
79+
return false;
80+
}
81+
82+
}
83+
84+
return true;
85+
}
86+
},
87+
IPv6 {
88+
@Override
89+
public boolean isValid(CharSequence charSequence) {
90+
String ipAddress = charSequence.toString();
91+
92+
int compressionIndex = ipAddress.indexOf( "::" );
93+
if ( compressionIndex != -1 && ipAddress.lastIndexOf( "::" ) != compressionIndex ) {
94+
return false;
95+
}
96+
97+
boolean startsWithCompression = ipAddress.startsWith( "::" );
98+
boolean endsWithCompression = ipAddress.endsWith( "::" );
99+
if ( ( ipAddress.startsWith( ":" ) && !startsWithCompression ) || ( ipAddress.endsWith( ":" ) && !endsWithCompression ) ) {
100+
return false;
101+
}
102+
103+
String[] parts = ipAddress.split( ":" );
104+
boolean hasCompression = compressionIndex != -1;
105+
106+
if ( hasCompression ) {
107+
List<String> partsList = Arrays.asList( parts );
108+
if ( endsWithCompression ) {
109+
partsList.add( "" );
110+
}
111+
else if ( startsWithCompression && !partsList.isEmpty() ) {
112+
partsList.remove( 0 );
113+
}
114+
parts = partsList.toArray( new String[0] );
115+
}
116+
117+
if ( parts.length > 8 ) {
118+
return false;
119+
}
120+
121+
int partsCount = 0;
122+
for ( int i = 0; i < parts.length; i++ ) {
123+
String part = parts[i];
124+
125+
if ( part.isBlank() ) {
126+
if ( i > 0 && parts[i - 1].isBlank() ) {
127+
return false;
128+
}
129+
}
130+
else if ( i == parts.length - 1 && part.contains( "." ) ) {
131+
if ( !IPv4.isValid( part ) ) {
132+
return false;
133+
}
134+
partsCount += 2;
135+
}
136+
else {
137+
if ( part.length() > 4 ) {
138+
return false;
139+
}
140+
int num;
141+
try {
142+
num = Integer.parseInt( part, 16 );
143+
}
144+
catch (NumberFormatException e) {
145+
return false;
146+
}
147+
if ( num < 0 || num > 0xFFFF ) {
148+
return false;
149+
}
150+
partsCount++;
151+
}
152+
}
153+
154+
if ( partsCount > 8 || ( partsCount < 8 && !hasCompression ) ) {
155+
return false;
156+
}
157+
158+
return true;
159+
}
160+
},
161+
ANY {
162+
@Override
163+
public boolean isValid(CharSequence charSequence) {
164+
return IPv4.isValid( charSequence ) || IPv6.isValid( charSequence );
165+
}
166+
}
167+
}
168+
}

engine/src/main/java/org/hibernate/validator/internal/metadata/core/BuiltinConstraint.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ enum BuiltinConstraint {
5353
// Hibernate Validator specific constraints
5454
ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_CODE_POINT_LENGTH( "org.hibernate.validator.constraints.CodePointLength" ),
5555
ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_CURRENCY( "org.hibernate.validator.constraints.Currency" ),
56+
ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_IP_ADDRESS( "org.hibernate.validator.constraints.IpAddress" ),
5657
ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_ISBN( "org.hibernate.validator.constraints.ISBN" ),
5758
ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_LENGTH( "org.hibernate.validator.constraints.Length" ),
5859
ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_LUHN_CHECK( "org.hibernate.validator.constraints.LuhnCheck" ),

engine/src/main/java/org/hibernate/validator/internal/metadata/core/ConstraintHelper.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import static org.hibernate.validator.internal.metadata.core.BuiltinConstraint.ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_CREDIT_CARD_NUMBER;
3535
import static org.hibernate.validator.internal.metadata.core.BuiltinConstraint.ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_CURRENCY;
3636
import static org.hibernate.validator.internal.metadata.core.BuiltinConstraint.ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_EAN;
37+
import static org.hibernate.validator.internal.metadata.core.BuiltinConstraint.ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_IP_ADDRESS;
3738
import static org.hibernate.validator.internal.metadata.core.BuiltinConstraint.ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_ISBN;
3839
import static org.hibernate.validator.internal.metadata.core.BuiltinConstraint.ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_KOR_KORRRN;
3940
import static org.hibernate.validator.internal.metadata.core.BuiltinConstraint.ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_LENGTH;
@@ -107,6 +108,7 @@
107108
import org.hibernate.validator.constraints.Currency;
108109
import org.hibernate.validator.constraints.EAN;
109110
import org.hibernate.validator.constraints.ISBN;
111+
import org.hibernate.validator.constraints.IpAddress;
110112
import org.hibernate.validator.constraints.Length;
111113
import org.hibernate.validator.constraints.LuhnCheck;
112114
import org.hibernate.validator.constraints.Mod10Check;
@@ -327,6 +329,7 @@
327329
import org.hibernate.validator.internal.constraintvalidators.hv.CodePointLengthValidator;
328330
import org.hibernate.validator.internal.constraintvalidators.hv.EANValidator;
329331
import org.hibernate.validator.internal.constraintvalidators.hv.ISBNValidator;
332+
import org.hibernate.validator.internal.constraintvalidators.hv.IpAddressValidator;
330333
import org.hibernate.validator.internal.constraintvalidators.hv.LengthValidator;
331334
import org.hibernate.validator.internal.constraintvalidators.hv.LuhnCheckValidator;
332335
import org.hibernate.validator.internal.constraintvalidators.hv.Mod10CheckValidator;
@@ -754,6 +757,9 @@ protected Map<Class<? extends Annotation>, List<? extends ConstraintValidatorDes
754757
if ( enabledBuiltinConstraints.contains( ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_EAN ) ) {
755758
putBuiltinConstraint( tmpConstraints, EAN.class, EANValidator.class );
756759
}
760+
if ( enabledBuiltinConstraints.contains( ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_IP_ADDRESS ) ) {
761+
putBuiltinConstraint( tmpConstraints, IpAddress.class, IpAddressValidator.class );
762+
}
757763
if ( enabledBuiltinConstraints.contains( ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_ISBN ) ) {
758764
putBuiltinConstraint( tmpConstraints, ISBN.class, ISBNValidator.class );
759765
}

0 commit comments

Comments
 (0)