Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 7cd48f7

Browse files
Add ConditionalMandatory
- Update README.md - Change some of the unit test to utilize ParameterizedTest
1 parent 8523f9e commit 7cd48f7

File tree

10 files changed

+457
-271
lines changed

10 files changed

+457
-271
lines changed

‎README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@ The `@Age` annotation ensures that a `LocalDate` field represents an age within
2121

2222
The `@AtLeastOneNotBlank` annotation validates whether at least one of the specified fields is not blank (non-empty).
2323

24+
### ConditionalMandatory
25+
26+
The `@ConditionalMandatory` annotation validates whether required fields are present if specified field has the given
27+
value or values.
28+
29+
### DivisibleBy
30+
31+
The `@DivisibleBy` annotation ensures that a `Integer` field is divisible by the given divider
32+
2433
### ExcludedNumbers
2534

2635
The `@ExcludedNumbers` annotation checks that a field's value is not one of the specified excluded numbers.

‎src/main/java/memos/tutorials/customvalidation/controller/dto/UserRequestDTO.java

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,32 @@
22

33
import jakarta.validation.constraints.Max;
44
import jakarta.validation.constraints.Min;
5-
import jakarta.validation.constraints.NotBlank;
65
import jakarta.validation.constraints.NotNull;
76
import lombok.Data;
87
import lombok.NoArgsConstructor;
98
import memos.tutorials.customvalidation.controller.validation.*;
10-
import org.hibernate.validator.constraints.URL;
119

1210
import java.time.LocalDate;
1311

1412
@Data
1513
@NoArgsConstructor
1614
@AtLeastOneNotBlank(fields = {"id", "passport", "drivingLicence"})
15+
@ConditionalMandatory(field = "type", values = "BUSINESS", requires = "companyName")
16+
@ConditionalMandatory(field = "type", values = "PERSONAL", requires = {"firstName", "lastName"})
1717
public class UserRequestDTO {
18-
@NotBlank
18+
public enum AccountType {
19+
PERSONAL, BUSINESS
20+
}
21+
22+
@NotNull
23+
private AccountType type;
24+
1925
private String firstName;
2026

21-
@NotBlank
2227
private String lastName;
2328

29+
private String companyName;
30+
2431
private String id;
2532

2633
private String passport;
@@ -30,7 +37,6 @@ public class UserRequestDTO {
3037
@Age(min = 18, max = 65)
3138
private LocalDate birthDate;
3239

33-
@URL
3440
@ISO3166CountryCode
3541
private String country;
3642

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package memos.tutorials.customvalidation.controller.validation;
2+
3+
import java.lang.annotation.*;
4+
5+
@Documented
6+
@Target(ElementType.TYPE)
7+
@Retention(RetentionPolicy.RUNTIME)
8+
public @interface ConditionalMandatories {
9+
10+
ConditionalMandatory[] value();
11+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package memos.tutorials.customvalidation.controller.validation;
2+
3+
import jakarta.validation.Constraint;
4+
import jakarta.validation.Payload;
5+
import memos.tutorials.customvalidation.controller.validation.validator.ConditionalMandatoryConstraintValidator;
6+
7+
import java.lang.annotation.*;
8+
9+
/**
10+
* Annotated classes are validated to unsure that a field is mandatory only under specific conditions based on the value of another field.
11+
* <p>
12+
* {@code field} The name of the field whose value determines if other fields are mandatory.
13+
* {@code values} An array of values for the `field` that trigger the mandatory requirement.
14+
* {@code requires} An array of field names that become mandatory when the `field` has one of the specified `values`.
15+
* <p>
16+
* Usage Example:
17+
* <pre>
18+
* {@literal @}ConditionalMandatory(field = "field1", values = {"a", "b"}, requires = {"field2", "field3"})
19+
* public class Person {
20+
* private String field1;
21+
* private String field2;
22+
* private String field3;
23+
* // ...
24+
* }
25+
* </pre>
26+
* </p>
27+
*
28+
* @author <i> Memo's Tutorial</i>
29+
* @since 0.0.1
30+
*/
31+
32+
@Documented
33+
@Target(ElementType.TYPE)
34+
@Retention(RetentionPolicy.RUNTIME)
35+
@Repeatable(ConditionalMandatories.class)
36+
@Constraint(validatedBy = ConditionalMandatoryConstraintValidator.class)
37+
public @interface ConditionalMandatory {
38+
39+
String field();
40+
41+
String[] values();
42+
43+
String[] requires();
44+
45+
String message() default "{requires} must be present when {field} values are {values}";
46+
47+
Class<?>[] groups() default {};
48+
49+
Class<? extends Payload>[] payload() default {};
50+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package memos.tutorials.customvalidation.controller.validation.validator;
2+
3+
import jakarta.validation.ConstraintValidator;
4+
import jakarta.validation.ConstraintValidatorContext;
5+
import memos.tutorials.customvalidation.controller.validation.ConditionalMandatory;
6+
7+
import java.lang.reflect.Field;
8+
import java.util.Arrays;
9+
10+
import static org.springframework.util.StringUtils.hasText;
11+
12+
public class ConditionalMandatoryConstraintValidator implements ConstraintValidator<ConditionalMandatory, Object> {
13+
private String field;
14+
15+
private String[] values;
16+
17+
private String[] requiredFields;
18+
19+
@Override
20+
public void initialize(ConditionalMandatory constraintAnnotation) {
21+
this.field = constraintAnnotation.field();
22+
this.values = constraintAnnotation.values();
23+
this.requiredFields = constraintAnnotation.requires();
24+
}
25+
26+
@Override
27+
public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) {
28+
try {
29+
Object conditionalFieldValue = getFieldValue(value, field);
30+
if (conditionalFieldValue != null && Arrays.asList(values).contains(conditionalFieldValue.toString())) {
31+
return validateRequiredFields(value, requiredFields);
32+
}
33+
return true;
34+
} catch (Exception e) {
35+
throw new RuntimeException(
36+
"Invalid field name, type or access while performing ConditionalMandatory validation", e);
37+
}
38+
}
39+
40+
private Object getFieldValue(Object obj, String fieldName) throws NoSuchFieldException, IllegalAccessException {
41+
Class<?> clazz = obj.getClass();
42+
Field field = clazz.getDeclaredField(fieldName);
43+
field.setAccessible(true);
44+
return field.get(obj);
45+
}
46+
47+
private boolean validateRequiredFields(Object obj, String[] fields) throws NoSuchFieldException, IllegalAccessException {
48+
for (String field : fields) {
49+
Object fieldValue = getFieldValue(obj, field);
50+
if (fieldValue == null || !hasText(fieldValue.toString())) {
51+
return false;
52+
}
53+
}
54+
return true;
55+
}
56+
}

‎src/test/java/memos/tutorials/customvalidation/controller/UserControllerTest.java

Lines changed: 58 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,17 @@
44
import org.junit.jupiter.api.AfterEach;
55
import org.junit.jupiter.api.BeforeEach;
66
import org.junit.jupiter.api.Test;
7+
import org.junit.jupiter.params.ParameterizedTest;
8+
import org.junit.jupiter.params.provider.MethodSource;
79
import org.mockito.InjectMocks;
810
import org.mockito.MockitoAnnotations;
911
import org.springframework.http.MediaType;
1012
import org.springframework.test.web.servlet.MockMvc;
1113

1214
import java.time.LocalDate;
15+
import java.util.ArrayList;
16+
import java.util.List;
17+
import java.util.stream.Stream;
1318

1419
import static memos.tutorials.customvalidation.TestUtils.buildMockMvc;
1520
import static memos.tutorials.customvalidation.TestUtils.getObjectMapper;
@@ -49,140 +54,89 @@ void shouldReturn204() throws Exception {
4954

5055
}
5156

52-
@Test
53-
void shouldReturn400WhenLuckyNumberNotValid() throws Exception {
54-
// given
55-
UserRequestDTO dto = getValidUserRequestDTO();
56-
dto.setPassport("790916a2-75db-4744-a2c5-6127c1271e31");
57-
dto.setLuckyNumber(13);
58-
59-
// when
57+
@ParameterizedTest
58+
@MethodSource("invalidDTOProvider")
59+
void shouldReturn400(UserRequestDTO dto) throws Exception {
6060
mockMvc.perform(post("/validation-examples")
6161
.contentType(MediaType.APPLICATION_JSON)
6262
.content(getObjectMapper().writeValueAsString(dto)))
6363
.andExpect(status().isBadRequest());
64-
6564
}
6665

67-
@Test
68-
void shouldReturn400WhenMaxDataSizeNotValid() throws Exception {
69-
// given
66+
private static Stream<UserRequestDTO> invalidDTOProvider() {
67+
List<UserRequestDTO> requests = new ArrayList<>();
68+
69+
// Invalid lucky number
7070
UserRequestDTO dto = getValidUserRequestDTO();
71-
dto.setDrivingLicence("790916a2-75db-4744-a2c5-6127c1271e31");
72-
dto.setFibonacci(null);
71+
dto.setLuckyNumber(13);
72+
requests.add(dto);
7373

74+
// Invalid max data size
75+
dto = getValidUserRequestDTO();
7476
dto.setMaxDataSize(64);
77+
requests.add(dto);
7578

76-
// when
77-
mockMvc.perform(post("/validation-examples")
78-
.contentType(MediaType.APPLICATION_JSON)
79-
.content(getObjectMapper().writeValueAsString(dto)))
80-
.andExpect(status().isBadRequest());
81-
82-
}
83-
84-
@Test
85-
void shouldReturn400WhenFibonacciNotValid() throws Exception {
86-
// given
87-
UserRequestDTO dto = getValidUserRequestDTO();
88-
dto.setLuckyNumber(null);
89-
79+
// Invalid fibonacci
80+
dto = getValidUserRequestDTO();
9081
dto.setFibonacci(4L);
82+
requests.add(dto);
9183

92-
// when
93-
mockMvc.perform(post("/validation-examples")
94-
.contentType(MediaType.APPLICATION_JSON)
95-
.content(getObjectMapper().writeValueAsString(dto)))
96-
.andExpect(status().isBadRequest());
97-
98-
}
99-
100-
@Test
101-
void shouldReturn400WhenSelectiveMandatoryFieldsNotPresent() throws Exception {
102-
// given
103-
UserRequestDTO dto = getValidUserRequestDTO();
104-
84+
// Invalid selective mandatory
85+
dto = getValidUserRequestDTO();
10586
dto.setPassport("");
10687
dto.setId(null);
10788
dto.setDrivingLicence("");
89+
requests.add(dto);
10890

109-
// when
110-
mockMvc.perform(post("/validation-examples")
111-
.contentType(MediaType.APPLICATION_JSON)
112-
.content(getObjectMapper().writeValueAsString(dto)))
113-
.andExpect(status().isBadRequest());
114-
115-
}
116-
117-
@Test
118-
void shouldReturn400WhenCountryNotValid() throws Exception {
119-
// given
120-
UserRequestDTO dto = getValidUserRequestDTO();
121-
122-
dto.setCountry("TUR");
123-
124-
// when
125-
mockMvc.perform(post("/validation-examples")
126-
.contentType(MediaType.APPLICATION_JSON)
127-
.content(getObjectMapper().writeValueAsString(dto)))
128-
.andExpect(status().isBadRequest());
129-
}
130-
131-
@Test
132-
void shouldReturn400WhenAgeNotValid() throws Exception {
133-
// given
134-
UserRequestDTO dto = getValidUserRequestDTO();
91+
// Invalid country
92+
dto = getValidUserRequestDTO();
93+
dto.setCountry("XXX");
94+
requests.add(dto);
13595

96+
// Invalid age - younger
97+
dto = getValidUserRequestDTO();
13698
dto.setBirthDate(LocalDate.now());
99+
requests.add(dto);
137100

138-
// when
139-
mockMvc.perform(post("/validation-examples")
140-
.contentType(MediaType.APPLICATION_JSON)
141-
.content(getObjectMapper().writeValueAsString(dto)))
142-
.andExpect(status().isBadRequest());
143-
144-
// given
101+
// Invalid age - older
145102
dto = getValidUserRequestDTO();
146-
147103
dto.setBirthDate(LocalDate.of(1900, 1, 1));
104+
requests.add(dto);
148105

149-
// when
150-
mockMvc.perform(post("/validation-examples")
151-
.contentType(MediaType.APPLICATION_JSON)
152-
.content(getObjectMapper().writeValueAsString(dto)))
153-
.andExpect(status().isBadRequest());
154-
}
155-
156-
@Test
157-
void shouldReturn400WhenCombinedValidationNotValid() throws Exception {
158-
// given
159-
UserRequestDTO dto = getValidUserRequestDTO();
160-
106+
// Invalid min, max, excluded number
107+
dto = getValidUserRequestDTO();
161108
dto.setCombinedValidation(6);
109+
requests.add(dto);
162110

163-
// when
164-
mockMvc.perform(post("/validation-examples")
165-
.contentType(MediaType.APPLICATION_JSON)
166-
.content(getObjectMapper().writeValueAsString(dto)))
167-
.andExpect(status().isBadRequest());
168-
}
111+
// Invalid number divisible by 3
112+
dto = getValidUserRequestDTO();
113+
dto.setSubscriptionDuration(2);
114+
requests.add(dto);
169115

170-
@Test
171-
void shouldReturn400WhenSubscriptionDurationNotDivisibleBy3() throws Exception {
172-
// given
173-
UserRequestDTO dto = getValidUserRequestDTO();
116+
// Invalid company name
117+
dto = getValidUserRequestDTO();
118+
dto.setType(UserRequestDTO.AccountType.BUSINESS);
119+
dto.setCompanyName("");
120+
requests.add(dto);
174121

175-
dto.setSubscriptionDuration(2);
122+
// Invalid personal first name
123+
dto = getValidUserRequestDTO();
124+
dto.setType(UserRequestDTO.AccountType.PERSONAL);
125+
dto.setFirstName("");
126+
requests.add(dto);
176127

177-
// when
178-
mockMvc.perform(post("/validation-examples")
179-
.contentType(MediaType.APPLICATION_JSON)
180-
.content(getObjectMapper().writeValueAsString(dto)))
181-
.andExpect(status().isBadRequest());
128+
// Invalid personal last name
129+
dto = getValidUserRequestDTO();
130+
dto.setType(UserRequestDTO.AccountType.PERSONAL);
131+
dto.setLastName("");
132+
requests.add(dto);
133+
134+
return requests.stream();
182135
}
183136

184-
private UserRequestDTO getValidUserRequestDTO() {
137+
private staticUserRequestDTO getValidUserRequestDTO() {
185138
UserRequestDTO dto = new UserRequestDTO();
139+
dto.setType(UserRequestDTO.AccountType.PERSONAL);
186140
dto.setFirstName("Memo's");
187141
dto.setLastName("Tutorials");
188142
dto.setId("790916a2-75db-4744-a2c5-6127c1271e31");

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /