BAEL-7119: Localized Validation Messages in REST (#14996)

* BAEL-7119: Localized Validation Messages in REST

* BAEL-7119: Localized Validation Messages in REST
- Remove unused imports
This commit is contained in:
Manfred 2023-10-31 23:41:15 +00:00 committed by GitHub
parent 0422349dab
commit 2f9acfe1d1
21 changed files with 562 additions and 0 deletions

View File

@ -91,6 +91,10 @@
<artifactId>aspectjweaver</artifactId>
<version>${aspectjweaver.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>

View File

@ -0,0 +1,15 @@
package com.baeldung.restvalidation;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@ComponentScan(basePackages = { "com.baeldung.restvalidation" })
public class RestValidationApplication {
public static void main(String[] args) {
SpringApplication.run(RestValidationApplication.class, args);
}
}

View File

@ -0,0 +1,38 @@
package com.baeldung.restvalidation.config;
import javax.validation.MessageInterpolator;
import org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.validation.beanvalidation.MessageSourceResourceBundleLocator;
@Configuration
public class MessageConfig {
@Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasename("classpath:CustomValidationMessages");
messageSource.setDefaultEncoding("UTF-8");
return messageSource;
}
@Bean
public MessageInterpolator getMessageInterpolator(MessageSource messageSource) {
MessageSourceResourceBundleLocator resourceBundleLocator = new MessageSourceResourceBundleLocator(messageSource);
ResourceBundleMessageInterpolator messageInterpolator = new ResourceBundleMessageInterpolator(resourceBundleLocator);
return new RecursiveLocaleContextMessageInterpolator(messageInterpolator);
}
@Bean
public LocalValidatorFactoryBean getValidator(MessageInterpolator messageInterpolator) {
LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
bean.setMessageInterpolator(messageInterpolator);
return bean;
}
}

View File

@ -0,0 +1,36 @@
package com.baeldung.restvalidation.config;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.validation.MessageInterpolator;
import org.hibernate.validator.messageinterpolation.AbstractMessageInterpolator;
import org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator;
public class RecursiveLocaleContextMessageInterpolator extends AbstractMessageInterpolator {
private static final Pattern PATTERN_PLACEHOLDER = Pattern.compile("\\{([^}]+)\\}");
private final MessageInterpolator interpolator;
public RecursiveLocaleContextMessageInterpolator(ResourceBundleMessageInterpolator interpolator) {
this.interpolator = interpolator;
}
@Override
public String interpolate(MessageInterpolator.Context context, Locale locale, String message) {
int level = 0;
while (containsPlaceholder(message) && (level++ < 2)) {
message = this.interpolator.interpolate(message, context, locale);
}
return message;
}
private boolean containsPlaceholder(String code) {
Matcher matcher = PATTERN_PLACEHOLDER.matcher(code);
return matcher.find();
}
}

View File

@ -0,0 +1,12 @@
package com.baeldung.restvalidation.response;
import lombok.*;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class InputFieldError {
private String field;
private String message;
}

View File

@ -0,0 +1,17 @@
package com.baeldung.restvalidation.response;
import com.fasterxml.jackson.annotation.JsonInclude;
import java.util.List;
import lombok.*;
@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class UpdateUserResponse {
private List<InputFieldError> fieldErrors;
}

View File

@ -0,0 +1,15 @@
package com.baeldung.restvalidation.service1;
import javax.validation.constraints.NotEmpty;
import lombok.*;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
@NotEmpty
private String email;
}

View File

@ -0,0 +1,36 @@
package com.baeldung.restvalidation.service1;
import org.springframework.http.*;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
import javax.validation.Valid;
import com.baeldung.restvalidation.response.InputFieldError;
import com.baeldung.restvalidation.response.UpdateUserResponse;
@RestController
public class UserService1 {
@PutMapping(value = "/user1", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<UpdateUserResponse> updateUser(@RequestBody @Valid User user,
BindingResult bindingResult) {
if (bindingResult.hasFieldErrors()) {
List<InputFieldError> fieldErrorList = bindingResult.getFieldErrors().stream()
.map(error -> new InputFieldError(error.getField(), error.getDefaultMessage()))
.collect(Collectors.toList());
UpdateUserResponse updateResponse = new UpdateUserResponse(fieldErrorList);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(updateResponse);
}
else {
// Update logic...
return ResponseEntity.status(HttpStatus.OK).build();
}
}
}

View File

@ -0,0 +1,15 @@
package com.baeldung.restvalidation.service2;
import javax.validation.constraints.NotEmpty;
import lombok.*;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
@NotEmpty(message = "{validation.email.notEmpty}")
private String email;
}

View File

@ -0,0 +1,40 @@
package com.baeldung.restvalidation.service2;
import java.util.List;
import java.util.stream.Collectors;
import javax.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.baeldung.restvalidation.response.InputFieldError;
import com.baeldung.restvalidation.response.UpdateUserResponse;
@RestController
public class UserService2 {
@PutMapping(value = "/user2", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<UpdateUserResponse> updateUser(@RequestBody @Valid User user,
BindingResult bindingResult) {
if (bindingResult.hasFieldErrors()) {
List<InputFieldError> fieldErrorList = bindingResult.getFieldErrors().stream()
.map(error -> new InputFieldError(error.getField(), error.getDefaultMessage()))
.collect(Collectors.toList());
UpdateUserResponse updateResponse = new UpdateUserResponse(fieldErrorList);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(updateResponse);
}
else {
// Update logic...
return ResponseEntity.status(HttpStatus.OK).build();
}
}
}

View File

@ -0,0 +1,32 @@
package com.baeldung.restvalidation.service3;
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 javax.validation.Constraint;
import javax.validation.Payload;
@Documented
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Constraint(validatedBy = { FieldNotEmptyValidator.class })
public @interface FieldNotEmpty {
String message() default "{validation.notEmpty}";
String field() default "Field";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

View File

@ -0,0 +1,16 @@
package com.baeldung.restvalidation.service3;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class FieldNotEmptyValidator implements ConstraintValidator<FieldNotEmpty, Object> {
private String message;
private String field;
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
return (value != null && !value.toString().trim().isEmpty());
}
}

View File

@ -0,0 +1,13 @@
package com.baeldung.restvalidation.service3;
import lombok.*;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
@FieldNotEmpty(message = "{validation.notEmpty}", field = "{field.personalEmail}")
private String email;
}

View File

@ -0,0 +1,40 @@
package com.baeldung.restvalidation.service3;
import java.util.List;
import java.util.stream.Collectors;
import javax.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.baeldung.restvalidation.response.InputFieldError;
import com.baeldung.restvalidation.response.UpdateUserResponse;
@RestController
public class UserService3 {
@PutMapping(value = "/user3", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<UpdateUserResponse> updateUser(@RequestBody @Valid User user,
BindingResult bindingResult) {
if (bindingResult.hasFieldErrors()) {
List<InputFieldError> fieldErrorList = bindingResult.getFieldErrors().stream()
.map(error -> new InputFieldError(error.getField(), error.getDefaultMessage()))
.collect(Collectors.toList());
UpdateUserResponse updateResponse = new UpdateUserResponse(fieldErrorList);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(updateResponse);
}
else {
// Update logic...
return ResponseEntity.status(HttpStatus.OK).build();
}
}
}

View File

@ -0,0 +1,3 @@
field.personalEmail=Personal Email
validation.notEmpty={field} cannot be empty
validation.email.notEmpty=Email cannot be empty

View File

@ -0,0 +1,3 @@
field.personalEmail=個人電郵
validation.notEmpty={field}不能是空白
validation.email.notEmpty=電郵不能留空

View File

@ -0,0 +1 @@
javax.validation.constraints.NotEmpty.message=The field cannot be empty

View File

@ -0,0 +1 @@
javax.validation.constraints.NotEmpty.message=本欄不能留空

View File

@ -0,0 +1,75 @@
package com.baeldung.restvalidation.service1;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.Objects;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.*;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import com.baeldung.restvalidation.RestValidationApplication;
import com.baeldung.restvalidation.response.InputFieldError;
import com.baeldung.restvalidation.response.UpdateUserResponse;
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = RestValidationApplication.class)
class UserService1IntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
void whenUpdateValidEmail_thenReturnsOK() {
// When
ResponseEntity<UpdateUserResponse> responseEntity = updateUser(new User("test@email.com"), null);
// Then
assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
}
@Test
void whenUpdateEmptyEmail_thenReturnsErrorMessageInEnglish() {
// When
ResponseEntity<UpdateUserResponse> responseEntity = updateUser(new User(""), null);
// Then
InputFieldError error = responseEntity.getBody().getFieldErrors().get(0);
assertEquals(HttpStatus.BAD_REQUEST, responseEntity.getStatusCode());
assertEquals("The field cannot be empty", error.getMessage());
}
@Test
void whenUpdateEmptyEmailWithLanguageHeaderEqualsToZh_thenReturnsErrorMessageInChinese() {
// When
ResponseEntity<UpdateUserResponse> responseEntity = updateUser(new User(""), "zh-tw");
// Then
InputFieldError error = responseEntity.getBody().getFieldErrors().get(0);
assertEquals(HttpStatus.BAD_REQUEST, responseEntity.getStatusCode());
assertEquals("本欄不能留空", error.getMessage());
}
private ResponseEntity<UpdateUserResponse> updateUser(User user, String language) {
HttpHeaders headers = new HttpHeaders();
if (Objects.nonNull(language)) {
headers.set(HttpHeaders.ACCEPT_LANGUAGE, language);
}
return restTemplate.exchange(
"/user1",
HttpMethod.PUT,
new HttpEntity<>(user, headers),
UpdateUserResponse.class
);
}
}

View File

@ -0,0 +1,75 @@
package com.baeldung.restvalidation.service2;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.Objects;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.*;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import com.baeldung.restvalidation.RestValidationApplication;
import com.baeldung.restvalidation.response.InputFieldError;
import com.baeldung.restvalidation.response.UpdateUserResponse;
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = RestValidationApplication.class)
class UserService2IntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
void whenUpdateValidEmail_thenReturnsOK() {
// When
ResponseEntity<UpdateUserResponse> responseEntity = updateUser(new User("test@email.com"), null);
// Then
assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
}
@Test
void whenUpdateEmptyEmail_thenReturnsErrorMessageInEnglish() {
// When
ResponseEntity<UpdateUserResponse> responseEntity = updateUser(new User(""), null);
// Then
InputFieldError error = responseEntity.getBody().getFieldErrors().get(0);
assertEquals(HttpStatus.BAD_REQUEST, responseEntity.getStatusCode());
assertEquals("Email cannot be empty", error.getMessage());
}
@Test
void whenUpdateEmptyEmailWithLanguageHeaderEqualsToZh_thenReturnsErrorMessageInChinese() {
// When
ResponseEntity<UpdateUserResponse> responseEntity = updateUser(new User(""), "zh-tw");
// Then
InputFieldError error = responseEntity.getBody().getFieldErrors().get(0);
assertEquals(HttpStatus.BAD_REQUEST, responseEntity.getStatusCode());
assertEquals("電郵不能留空", error.getMessage());
}
private ResponseEntity<UpdateUserResponse> updateUser(User user, String language) {
HttpHeaders headers = new HttpHeaders();
if (Objects.nonNull(language)) {
headers.set(HttpHeaders.ACCEPT_LANGUAGE, language);
}
return restTemplate.exchange(
"/user2",
HttpMethod.PUT,
new HttpEntity<>(user, headers),
UpdateUserResponse.class
);
}
}

View File

@ -0,0 +1,75 @@
package com.baeldung.restvalidation.service3;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.Objects;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.*;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import com.baeldung.restvalidation.RestValidationApplication;
import com.baeldung.restvalidation.response.InputFieldError;
import com.baeldung.restvalidation.response.UpdateUserResponse;
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = RestValidationApplication.class)
class UserService3IntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
void whenUpdateValidEmail_thenReturnsOK() {
// When
ResponseEntity<UpdateUserResponse> responseEntity = updateUser(new User("test@email.com"), null);
// Then
assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
}
@Test
void whenUpdateEmptyEmail_thenReturnsErrorMessageInEnglish() {
// When
ResponseEntity<UpdateUserResponse> responseEntity = updateUser(new User(""), null);
// Then
InputFieldError error = responseEntity.getBody().getFieldErrors().get(0);
assertEquals(HttpStatus.BAD_REQUEST, responseEntity.getStatusCode());
assertEquals("Personal Email cannot be empty", error.getMessage());
}
@Test
void whenUpdateEmptyEmailWithLanguageHeaderEqualsToZh_thenReturnsErrorMessageInChinese() {
// When
ResponseEntity<UpdateUserResponse> responseEntity = updateUser(new User(""), "zh-tw");
// Then
InputFieldError error = responseEntity.getBody().getFieldErrors().get(0);
assertEquals(HttpStatus.BAD_REQUEST, responseEntity.getStatusCode());
assertEquals("個人電郵不能是空白", error.getMessage());
}
private ResponseEntity<UpdateUserResponse> updateUser(User user, String language) {
HttpHeaders headers = new HttpHeaders();
if (Objects.nonNull(language)) {
headers.set(HttpHeaders.ACCEPT_LANGUAGE, language);
}
return restTemplate.exchange(
"/user3",
HttpMethod.PUT,
new HttpEntity<>(user, headers),
UpdateUserResponse.class
);
}
}