Skip to content

Commit d185a94

Browse files
authored
[fit] Enhance FIT's i18n capabilities by parsing locales from HTTP requests. (#275)
* [fit] 实现请求地区解析 * [fit] 实现国际化消息处理器,完善测试 * [fit] 完善注释 * [fit] 完善注释 * [fit] 修改校验处理默认使用的插值器 * [fit] 修改校验处理插值器,提供默认地区设置功能 * 初步实现 * 可用的多插件地区解析实现 * 完善注释 * 恢复样例 * 修改实现 * 完善代码 * 删除pom中的旧实现残留 * 修改错误方法名,为工具类提供测试方法。 * 修改格式。 * 修改格式 * 删除localecontext * 修复逻辑错误,完善格式 * 完善格式 * 修复错误 * 修复错误 * 完善测试 * 修复错误 * 为插值方法提供null值保护 * 完善格式以及优化null检查
1 parent d31f77c commit d185a94

File tree

21 files changed

+1061
-197
lines changed

21 files changed

+1061
-197
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved.
3+
* This file is a part of the ModelEngine Project.
4+
* Licensed under the MIT License. See License.txt in the project root for license information.
5+
*--------------------------------------------------------------------------------------------*/
6+
7+
package modelengine.fitframework.validation;
8+
9+
import jakarta.validation.MessageInterpolator;
10+
import modelengine.fitframework.inspection.Validation;
11+
import modelengine.fitframework.util.ObjectUtils;
12+
import modelengine.fitframework.util.i18n.LocaleContextHolder;
13+
14+
import org.hibernate.validator.messageinterpolation.ParameterMessageInterpolator;
15+
16+
import java.util.Locale;
17+
18+
/**
19+
* 检验消息处理的代理类。
20+
* <p>
21+
* 从 {@link LocaleContextHolder} 中获取当前线程设置的 {@link Locale} 并委托 {@link MessageInterpolator} 去处理消息。
22+
* </p>
23+
*
24+
* @author 阮睿
25+
* @since 2025-07-31
26+
*/
27+
public class LocaleContextMessageInterpolator implements MessageInterpolator {
28+
private final MessageInterpolator targetInterpolator;
29+
private Locale locale;
30+
31+
/**
32+
* 构造函数。
33+
*
34+
* @param targetInterpolator 表示目标检验消息处理对象的 {@link MessageInterpolator}。
35+
*/
36+
public LocaleContextMessageInterpolator(MessageInterpolator targetInterpolator) {
37+
this.targetInterpolator = targetInterpolator;
38+
this.locale = Locale.getDefault();
39+
}
40+
41+
/**
42+
* 构造函数,默认使用 {@link ParameterMessageInterpolator} 作为目标检验消息处理对象。
43+
*/
44+
public LocaleContextMessageInterpolator() {
45+
this.targetInterpolator = new ParameterMessageInterpolator();
46+
this.locale = Locale.getDefault();
47+
}
48+
49+
/**
50+
* 构造函数。
51+
*
52+
* @param locale 表示当前设置默认的 {@link Locale}。
53+
*/
54+
public LocaleContextMessageInterpolator(Locale locale) {
55+
this.targetInterpolator = new ParameterMessageInterpolator();
56+
this.locale = ObjectUtils.getIfNull(locale, Locale::getDefault);
57+
}
58+
59+
/**
60+
* 构造函数。
61+
*
62+
* @param targetInterpolator 表示目标检验消息处理对象的 {@link MessageInterpolator}。
63+
* @param locale 表示当前设置默认的 {@link Locale}。
64+
*/
65+
public LocaleContextMessageInterpolator(MessageInterpolator targetInterpolator, Locale locale) {
66+
this.targetInterpolator = targetInterpolator;
67+
this.locale = ObjectUtils.getIfNull(locale, Locale::getDefault);
68+
}
69+
70+
/**
71+
* 设置默认的 {@link Locale}。
72+
*
73+
* @param locale 默认设置的 {@link Locale}。
74+
*/
75+
public void setLocale(Locale locale) {
76+
this.locale = ObjectUtils.getIfNull(locale, Locale::getDefault);
77+
}
78+
79+
@Override
80+
public String interpolate(String messageTemplate, Context context) {
81+
if (LocaleContextHolder.getLocale() != null) {
82+
return this.targetInterpolator.interpolate(messageTemplate, context, LocaleContextHolder.getLocale());
83+
}
84+
return this.targetInterpolator.interpolate(messageTemplate, context, this.locale);
85+
}
86+
87+
@Override
88+
public String interpolate(String messageTemplate, Context context, Locale locale) {
89+
Validation.notNull(locale, "Locale cannot be null.");
90+
return this.targetInterpolator.interpolate(messageTemplate, context, locale);
91+
}
92+
}

framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/main/java/modelengine/fitframework/validation/LocaleMessageInterpolator.java

Lines changed: 0 additions & 86 deletions
This file was deleted.

framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/main/java/modelengine/fitframework/validation/ValidationHandler.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,10 @@
4343
public class ValidationHandler implements AutoCloseable {
4444
private final ValidatorFactory validatorFactory;
4545
private final Validator validator;
46-
private final LocaleMessageInterpolator messageInterpolator;
46+
private final LocaleContextMessageInterpolator messageInterpolator;
4747

4848
public ValidationHandler() {
49-
this.messageInterpolator = new LocaleMessageInterpolator();
49+
this.messageInterpolator = new LocaleContextMessageInterpolator();
5050
this.validatorFactory = Validation.byProvider(HibernateValidator.class)
5151
.configure()
5252
.messageInterpolator(this.messageInterpolator)
@@ -58,7 +58,7 @@ public ValidationHandler() {
5858
/**
5959
* 设置校验信息语言。
6060
*
61-
* @param locale 校验语言 {@link Locale}。
61+
* @param locale 表示校验语言的 {@link Locale}。
6262
*/
6363
public void setLocale(Locale locale) {
6464
this.messageInterpolator.setLocale(locale);
@@ -163,4 +163,4 @@ private boolean isJakartaConstraintAnnotation(Annotation annotation) {
163163
return "jakarta.validation".equals(packageName) && "Constraint".equals(className);
164164
});
165165
}
166-
}
166+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved.
3+
* This file is a part of the ModelEngine Project.
4+
* Licensed under the MIT License. See License.txt in the project root for license information.
5+
*--------------------------------------------------------------------------------------------*/
6+
7+
package modelengine.fitframework.validation;
8+
9+
import static org.assertj.core.api.Assertions.assertThat;
10+
11+
import modelengine.fit.http.client.HttpClassicClientResponse;
12+
import modelengine.fit.http.entity.Entity;
13+
import modelengine.fit.http.entity.ObjectEntity;
14+
import modelengine.fitframework.annotation.Fit;
15+
import modelengine.fitframework.test.annotation.MvcTest;
16+
import modelengine.fitframework.test.domain.mvc.MockMvc;
17+
import modelengine.fitframework.test.domain.mvc.request.MockMvcRequestBuilders;
18+
import modelengine.fitframework.test.domain.mvc.request.MockRequestBuilder;
19+
import modelengine.fitframework.validation.data.Company;
20+
import modelengine.fitframework.validation.data.LocaleValidationController;
21+
22+
import org.junit.jupiter.api.AfterEach;
23+
import org.junit.jupiter.api.DisplayName;
24+
import org.junit.jupiter.api.Test;
25+
26+
import java.io.IOException;
27+
import java.util.Map;
28+
29+
/**
30+
* 表示评估国际化校验的测试类。
31+
*
32+
* @author 阮睿
33+
* @since 2025-08-01
34+
*/
35+
@MvcTest(classes = {LocaleValidationController.class})
36+
@DisplayName("测试地区化验证消息功能")
37+
public class LocaleValidationControllerTest {
38+
@Fit
39+
private MockMvc mockMvc;
40+
41+
private HttpClassicClientResponse<?> response;
42+
43+
@AfterEach
44+
void teardown() throws IOException {
45+
if (this.response != null) {
46+
this.response.close();
47+
}
48+
}
49+
50+
@Test
51+
@DisplayName("测试法文地区的验证消息")
52+
void shouldReturnFrenchValidationMessage() {
53+
Company invalidCompany = new Company(null);
54+
55+
MockRequestBuilder requestBuilder = MockMvcRequestBuilders.post("/validation/locale/simple")
56+
.header("Accept-Language", "fr")
57+
.jsonEntity(invalidCompany)
58+
.responseType(Map.class);
59+
60+
this.response = this.mockMvc.perform(requestBuilder);
61+
// 获取JSON格式的错误信息
62+
String errorMessage = "";
63+
if (this.response.entity().isPresent()) {
64+
Entity entity = this.response.entity().get();
65+
if (entity instanceof ObjectEntity) {
66+
ObjectEntity<?> objectEntity = (ObjectEntity<?>) entity;
67+
Object errorObj = objectEntity.object();
68+
if (errorObj instanceof Map) {
69+
Map<String, Object> errorMap = (Map<String, Object>) errorObj;
70+
errorMessage =
71+
errorMap.get("error") != null ? errorMap.get("error").toString() : errorMap.toString();
72+
} else {
73+
errorMessage = errorObj.toString();
74+
}
75+
}
76+
}
77+
78+
assertThat(errorMessage).isEqualTo("validateSimpleParam.company.employees: ne doit pas être nul");
79+
assertThat(this.response.statusCode()).isEqualTo(500);
80+
}
81+
82+
@Test
83+
@DisplayName("测试英文地区的验证消息")
84+
void shouldReturnEnglishValidationMessage() {
85+
Company invalidCompany = new Company(null);
86+
87+
MockRequestBuilder requestBuilder = MockMvcRequestBuilders.post("/validation/locale/simple")
88+
.header("Accept-Language", "en-us")
89+
.jsonEntity(invalidCompany)
90+
.responseType(Map.class);
91+
92+
this.response = this.mockMvc.perform(requestBuilder);
93+
// 获取JSON格式的错误信息
94+
String errorMessage = "";
95+
if (this.response.entity().isPresent()) {
96+
Entity entity = this.response.entity().get();
97+
if (entity instanceof ObjectEntity) {
98+
ObjectEntity<?> objectEntity = (ObjectEntity<?>) entity;
99+
Object errorObj = objectEntity.object();
100+
if (errorObj instanceof Map) {
101+
Map<String, Object> errorMap = (Map<String, Object>) errorObj;
102+
errorMessage =
103+
errorMap.get("error") != null ? errorMap.get("error").toString() : errorMap.toString();
104+
} else {
105+
errorMessage = errorObj.toString();
106+
}
107+
}
108+
}
109+
110+
assertThat(errorMessage).isEqualTo("validateSimpleParam.company.employees: must not be null");
111+
assertThat(this.response.statusCode()).isEqualTo(500);
112+
}
113+
114+
@Test
115+
@DisplayName("测试URL参数指定地区")
116+
void shouldUseLocaleFromUrlParam() {
117+
Company invalidCompany = new Company(null);
118+
119+
MockRequestBuilder requestBuilder = MockMvcRequestBuilders.post("/validation/locale/simple")
120+
.param("locale", "en-US")
121+
.jsonEntity(invalidCompany)
122+
.responseType(Map.class);
123+
124+
this.response = this.mockMvc.perform(requestBuilder);
125+
126+
// 获取JSON格式的错误信息
127+
String errorMessage = "";
128+
if (this.response.entity().isPresent()) {
129+
Entity entity = this.response.entity().get();
130+
if (entity instanceof ObjectEntity) {
131+
ObjectEntity<?> objectEntity = (ObjectEntity<?>) entity;
132+
Object errorObj = objectEntity.object();
133+
if (errorObj instanceof Map) {
134+
Map<String, Object> errorMap = (Map<String, Object>) errorObj;
135+
errorMessage =
136+
errorMap.get("error") != null ? errorMap.get("error").toString() : errorMap.toString();
137+
} else {
138+
errorMessage = errorObj.toString();
139+
}
140+
}
141+
}
142+
143+
assertThat(errorMessage).isEqualTo("validateSimpleParam.company.employees: must not be null");
144+
assertThat(this.response.cookies().get("locale").isPresent());
145+
assertThat(this.response.cookies().get("locale").get().value()).isEqualTo("en-US");
146+
assertThat(this.response.statusCode()).isEqualTo(500);
147+
}
148+
}

framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/test/java/modelengine/fitframework/validation/ValidationHandlerTest.java

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,8 @@ class StudentGroupValidationTests {
341341
public void givenParametersThenGroupValidateHappened() {
342342
// 测试学生年龄验证 - 现在会抛出异常,因为使用了学生分组
343343
Method method = ReflectionUtils.getDeclaredMethod(GroupValidateService.StudentValidateService.class,
344-
"validateStudentAge", int.class);
344+
"validateStudentAge",
345+
int.class);
345346
Method handleValidatedMethod = ReflectionUtils.getDeclaredMethod(ValidationHandler.class,
346347
"handle",
347348
JoinPoint.class,
@@ -755,4 +756,21 @@ void testRangeBigDecimalValidation() {
755756
ConstraintViolationException exception = invokeHandleMethod(method, new Object[] {new BigDecimal("5.5")});
756757
assertThat(exception.getMessage()).contains("需要在10和100之间");
757758
}
758-
}
759+
760+
@Nested
761+
@DisplayName("测试 Locale 默认值为 null 时的情况")
762+
public class ValidationHandlerNullTest {
763+
@BeforeEach
764+
void setUp() {
765+
ValidationHandlerTest.this.handler.setLocale(null);
766+
}
767+
768+
@Test
769+
@DisplayName("测试@Null注解")
770+
void testNullValidation() {
771+
Method method = ReflectionUtils.getDeclaredMethod(ValidateService.class, "testNull", String.class);
772+
ConstraintViolationException exception = invokeHandleMethod(method, new Object[] {"not null"});
773+
assertThat(exception.getMessage()).isNotNull();
774+
}
775+
}
776+
}

0 commit comments

Comments
 (0)